「笔记」Link Cut Tree

写在前面

世界上怎么会有这么优美的数据结构!

这是一篇 Rewrite,「笔记」LCT 的 1.0 版本已经被削除了。

前置知识 Splay树链剖分,请务必充分理解它们的思想。

本文是具有主观性质的一些理解,用于自我总结复习,Link Cut Tree 初学者请移步其它博客。作者水平有限,若有不当之处请不吝赐教。
以及不要太在意 Tarjan 是怎么想到它的= =

动态树问题

给定一片森林,要求在线支持下列操作:

  1. 修改路径权值。
  2. 查询路径权值和。
  3. 断开并连接一些边,保证仍是一片森林。

要求单次操作复杂度 \(O(\log n)\) 级别。

若给定的森林是静态的,即不存在操作 3,该问题可以通过对原树的树链剖分,将树上路径/子树问题转化为基于 dfs 序的序列问题,从而使用序列静态数据结构维护

加入动态加/删边操作后,我们仍然考虑对原树进行树链剖分来维护路径信息。在静态问题中我们使用了静态数据结构维护树链。在这里可以考虑使用易于修改的 Splay 动态维护树链信息。
换句话说,LCT 是通过 Splay 动态维护树链剖分结构,从而将树上问题转化为 Splay 上的序列问题的一种数据结构。

定义

实链剖分

实链剖分是一种对原树的树链剖分的方式,它将树划分成了多条实链

  • 每个节点能且仅能存在于一条实链中。实链是节点深度递增的一条树链,实链与实链间通过虚边连接。
  • 实链中的边称为实边,称一条实边的儿子是父亲的实儿子。显然一个节点至多有一个实儿子,但可能有多个虚儿子。
  • 在实链剖分中,对于每个节点,仅记录其父亲 与 实儿子的信息,即“认父不认子”。

确定边的虚实的标准是人的主观能动,即可以灵活地“随意划分”,以达到灵活修改剖分结构的功能。以下是一个实链剖分的例子:

LCT 使用 Splay 来维护实链。每一条实链中的节点构成一棵 Splay。Splay 具有以下性质:

  • 每棵 Splay 维护原树中的一条实链,且中序遍历这棵 Splay 得到的点序列,从前到后对应原树自上到下的这条实链。
  • 原树每个节点与 Splay 节点一一对应。
  • 对于每一棵对应实链的 Splay,与一般 Splay 不同的是,其根节点也存在父亲节点,指向原树中其父亲节点。这条边对应着原树中两条实链间的虚边。所有 Splay 节点中仅有原树根对应节点的父节点为空。
  • 所有实链的 Splay 的父子关系构了一棵树,称为辅助树。由以上性质,辅助树可以体现原树中的所有父子关系,两棵原树的辅助树不同,两棵原树就不同。维护操作时仅需维护辅助树即可。

下图展示了上述实链剖分对应的一种合法的辅助树:

小结

  • 实链剖分是一种树链剖分的方式。实链是节点深度递增的一条树链,实链与实链间通过虚边连接。
  • 对于原树的实链:每棵 Splay 维护一条实链,其中序遍历对应自上到下的这条实链。
  • 对于原树的虚边:子节点是所在 Splay 的根,且其父亲指向虚边的父节点。但父节点的儿子不指向子节点。
  • 辅助树可以在满足辅助树、Splay 的性质下任意换根。

分步实现

以下以 P1501 Tree II 为例。

给定一 \(n\) 个节点的树,初始点权为 \(1\)。给定 \(m\) 次操作:

  1. 路径加。
  2. 路径乘。
  3. 删去一条边,再加入一条边,保证操作完之后仍然是一棵树。
  4. 查询路径和,答案对 \(51061\) 取模。

\(1\le n,m\le 10^5\)
2S,512MB。

定义及声明

#define f fa[now_]
#define ls son[now_][0]
#define rs son[now_][1]
const int kMaxNode = kN;
int fa[kMaxNode], son[kMaxNode][2], val[kMaxNode]; //Splay 的结构信息
LL siz[kMaxNode], sum[kMaxNode], tagplus[kMaxNode], tagprod[kMaxNode]; //子树大小,子树和,两种标记
bool tagrev[kMaxNode]; //Splay 的子树反转标记

Splay

void Pushup(int now_) { //维护子树和 和 子树大小
  sum[now_] = (sum[ls] + sum[rs] + val[now_]) % mod;
  siz[now_] = siz[ls] + siz[rs] + 1;
}
void PushReverse(int now_) { //子树反转标记,使得 Splay 节点的中序遍历反向。若原 splay 表示一条自顶向下的链,反转相当于将 splay 表示的链的边反向,父子关系互换。
  if (!now_) return;
  std::swap(ls, rs);
  tagrev[now_] ^= 1;
}
void PushPlus(int now_, LL val_) { //子树加
  if (!now_) return;
  val[now_] = (val[now_] + val_) % mod;
  sum[now_] = (sum[now_] + siz[now_] * val_ % mod) % mod;
  tagplus[now_] = (tagplus[now_] + val_) % mod;
}
void PushProd(int now_, LL val_) { //子树乘
  if (!now_) return;
  val[now_] = val[now_] * val_ % mod;
  sum[now_] = sum[now_] * val_ % mod;
  tagplus[now_] = tagplus[now_] * val_ % mod;
  tagprod[now_] = tagprod[now_] * val_ % mod;
}
void Pushdown(int now_) { //注意下放顺序
  LL plus = tagplus[now_], prod = tagprod[now_], rev = tagrev[now_];
  if (prod != 1) PushProd(ls, prod), PushProd(rs, prod);
  if (plus) PushPlus(ls, plus), PushPlus(rs, plus);
  if (rev) PushReverse(ls), PushReverse(rs);
  tagprod[now_] = 1, tagplus[now_] = 0, tagrev[now_] = 0;
}
bool IsRoot(int now_) { //判断 now_ 是否为当前 Splay 的根
  return son[f][0] != now_ && son[f][1] != now_;
}
bool WhichSon(int now_) {
  return son[f][1] == now_;
}
void Rotate(int now_) {
  int fa_ = f, w = WhichSon(now_);
  if (!IsRoot(f)) son[fa[f]][WhichSon(f)] = now_;
  f = fa[f];

  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_) { //将 Splay 路径上的所有标记下放
  if (!IsRoot(now_)) Update(f);
  Pushdown(now_);
}
void Splay(int now_) {
  Update(now_);
  for (; !IsRoot(now_); Rotate(now_)) {
    if (!IsRoot(f)) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_);
  }
}

Access 构造实链

上面提到,LCT 使用 Splay 来维护实链,可以将实链信息转化为 Splay 的子树信息。由此我们可以考虑把需要维护的对象拿出来,构成一个 Splay,再对该 Splay 进行操作即可。
Access 函数的作用,是将当前辅助树的根到指定节点的路径上所有点,构造成一个 Splay。即使得原树中由根到指定节点的链成为实链。

下图展示 Access(F) 后辅助树的变化。

实现时自下向上构建,舍弃父亲的原有右实儿子,更新为深度更深的链的根即可。

void Access(int now_) { //使得树中由根->now 的链成为实链,构造出由它们组成的 Splay,满足 Splay 的中序遍历深度递减的性质。
  //自下向上构建,舍弃父亲的原有右儿子,换成 -> last 的链。
  for (int last_ = 0; now_; last_ = now_, now_ = f) {
    Splay(now_), rs = last_;
    Pushup(now_);
  }
}

MakeRoot 换根

使指定节点成为原树的根。
画张图可以发现,换根相当于将根到指定节点的路径反向。

于是可以先使原树中由根到指定节点的链成为实链,再使它成为 Splay 的根。
此时的 Splay 表示一条自顶向下的链,路径反向等价于将 splay 表示的链的边反向,父子关系互换。这又等价于反转 Splay 的中序遍历,直接在 Splay 的根上打反转标记即可。

void MakeRoot(int now_) { //使 now 成为原树的根
  Access(now_); //先使得树中由根->now 的链成为实链,构造出由它们组成的 Splay。
  Splay(now_); //使 now 成为 splay 的根节点
  PushReverse(now_); //将根->now 的链反转,使 now 成为原树的根。原理参考 PushReverse 函数的注释。
}

Find 找到指定节点所在原树的根

构造出根到指定节点的 Splay 后,根据实链节点深度递增的性质,Splay 中序遍历的第一个元素即为原树的根。

int Find(int now_) { //找到 now_ 所在原树的根
  Access(now_); 
  Splay(now_);
  while (ls) Pushdown(now_), now_ = ls; //使得树中由根->now 的链成为实链,构造出由它们组成的 Splay,再找到 Splay 中序遍历的第一个元素,即为原树的根。
    
  Splay(now_); //为了下一步操作,把根再转回去
  return now_; 
}

Split 构造由路径组成的 Splay

使 \(x\) 成为根,构造出根到 \(y\) 的 Splay 即得。此时 Splay 根 \(y\) 的子树信息即为路径信息。
通过该操作可以取出指定的原树的一条路径放入 Splay 中,从而将路径问题转化为序列数据结构问题。

void Split(int x_, int y_) { //构造由路径 x->y 组成的 Splay
  MakeRoot(x_); //使 x 成为根,构造出根 -> y 的 Splay 即得,Splay 根的子树信息即为路径信息。
  Access(y_);
  Splay(y_);
}

使 \(x\) 成为根,将根的父亲指向 \(y\) 即可。注意判断两点之间是否已经连通。

void Link(int x_, int y_) { //加边 (x,y)
  MakeRoot(x_); //使 x 成为根,再给根一个父亲
  if (Find(y_) != x_) fa[x_] = y_;
}

Cut 删边

先使 \(x\) 成为根。再使用 Find 判断两点是否连通。

之后需要判断两点间是否有直接连边。
FindAccess(y) 之后,\(y\) 所在的 Splay 由 \(x\rightarrow y\) 的链构成。又 \(x\) 是 Splay 的根,\(y\) 的中序遍历恰好是 \(x\) 之后的一个,则若边 \((x,y)\) 存在,则 \(y\) 的位置只有一种可能:
\(y\)\(x\) 的右儿子,且 \(y\) 没有左儿子,按照下述代码判断即可。

void Cut(int x_, int y_) { //删边 (x,y)
  MakeRoot(x_); //使 x 成为根
  //Find(y_) != x_ 保证 y 与 x 连通
  //在 Find 函数中 Access(y) 之后,y 所在的 splay 由 x->y 的链构成。又 x 是 splay 的根,y 的中序遍历在 x 后,则若边 (x,y) 存在,则 y 的位置只有一种可能:  
  //y 是 x 的右儿子,且 y 没有左儿子。从而保证 y 在中序遍历中是 x 的后一个。
  if (Find(y_) != x_ || fa[y_] != x_ || son[y_][0]) return ;
  fa[y_] = son[x_][1] = 0; //断绝父子关系
  Pushup(x_);
}

路径修改 和 路径查询

Child's play。

void Modify(int x_, int y_, int val_, int type) { //路径修改
  Split(x_, y_); //给路径构成的 Splay 打标记
  if (!type) PushPlus(y_, val_);
  else PushProd(y_, val_);
}
LL Query(int x_, int y_) { //路径查询
  Split(x_, y_); //返回路径构成的 Splay 的信息
  return sum[y_];
}

封装模板

//知识点:LCT
/*
By:Luckyblock
*/
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], val[kMaxNode]; //Splay 的结构信息
  LL siz[kMaxNode], sum[kMaxNode], tagplus[kMaxNode], tagprod[kMaxNode]; //子树大小,子树和,两种标记
  bool tagrev[kMaxNode]; //Splay 的子树反转标记
  void Pushup(int now_) { //维护子树和 和 子树大小
    sum[now_] = (sum[ls] + sum[rs] + val[now_]) % mod;
    siz[now_] = siz[ls] + siz[rs] + 1;
  }
  void PushReverse(int now_) { //子树反转标记,使得 Splay 节点的中序遍历反向。若原 splay 表示一条自顶向下的链,反转相当于将 splay 表示的链的边反向,父子关系互换。
    if (!now_) return;
    std::swap(ls, rs);
    tagrev[now_] ^= 1;
  }
  void PushPlus(int now_, LL val_) {
    if (!now_) return;
    val[now_] = (val[now_] + val_) % mod;
    sum[now_] = (sum[now_] + siz[now_] * val_ % mod) % mod;
    tagplus[now_] = (tagplus[now_] + val_) % mod;
  }
  void PushProd(int now_, LL val_) {
    if (!now_) return;
    val[now_] = val[now_] * val_ % mod;
    sum[now_] = sum[now_] * val_ % mod;
    tagplus[now_] = tagplus[now_] * val_ % mod;
    tagprod[now_] = tagprod[now_] * val_ % mod;
  }
  void Pushdown(int now_) { //注意下放顺序
    LL plus = tagplus[now_], prod = tagprod[now_], rev = tagrev[now_];
    if (prod != 1) PushProd(ls, prod), PushProd(rs, prod);
    if (plus) PushPlus(ls, plus), PushPlus(rs, plus);
    if (rev) PushReverse(ls), PushReverse(rs);
    tagprod[now_] = 1, tagplus[now_] = 0, tagrev[now_] = 0;
  }
  bool IsRoot(int now_) { //判断 now_ 是否为当前 Splay 的根
    return son[f][0] != now_ && son[f][1] != now_;
  }
  bool WhichSon(int now_) {
    return son[f][1] == now_;
  }
  void Rotate(int now_) {
    int fa_ = f, w = WhichSon(now_);
    if (!IsRoot(f)) son[fa[f]][WhichSon(f)] = now_;
    f = fa[f];

    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_) { //将 Splay 路径上的所有标记下放
    if (!IsRoot(now_)) Update(f);
    Pushdown(now_);
  }
  void Splay(int now_) {
    Update(now_);
    for (; !IsRoot(now_); Rotate(now_)) {
      if (!IsRoot(f)) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_);
    }
  }
  void Access(int now_) { //使得树中由根->now 的链成为实链,构造出由它们组成的 Splay,满足 Splay 的中序遍历深度递减的性质。
    //自下向上构建,舍弃父亲的原有右儿子,换成 -> last 的链。
    for (int last_ = 0; now_; last_ = now_, now_ = f) {
      Splay(now_), rs = last_;
      Pushup(now_);
    }
  }
  void MakeRoot(int now_) { //使 now 成为原树的根
    Access(now_); //先使得树中由根->now 的链成为实链,构造出由它们组成的 Splay。
    Splay(now_); //使 now 成为 splay 的根节点
    PushReverse(now_); //将根->now 的链反转,使 now 成为原树的根。原理参考 PushReverse 函数的注释。
  }
  int Find(int now_) { //找到 now_ 所在原树的根
    Access(now_); 
    Splay(now_);
    while (ls) Pushdown(now_), now_ = ls; //使得树中由根->now 的链成为实链,构造出由它们组成的 Splay,再找到 Splay 中序遍历的第一个元素,即为原树的根。
    
    Splay(now_); //为了下一步操作,把根再转回去
    return now_; 
  }
  void Split(int x_, int y_) { //构造由路径 x->y 组成的 Splay
    MakeRoot(x_); //使 x 成为根,构造出根 -> y 的 Splay 即得,Splay 根的子树信息即为路径信息。
    Access(y_);
    Splay(y_);
  }
  void Link(int x_, int y_) { //加边 (x,y)
    MakeRoot(x_); //使 x 成为根,再给根一个父亲
    if (Find(y_) != x_) fa[x_] = y_;
  }
  void Cut(int x_, int y_) { //删边 (x,y)
    MakeRoot(x_); //使 x 成为根
    //Find(y_) != x_ 保证 y 与 x 连通
    //在 Find 函数中 Access(y) 之后,y 所在的 splay 由 x->y 的链构成。又 x 是 splay 的根,y 的中序遍历在 x 后,则若边 (x,y) 存在,则 y 的位置只有一种可能:  
    //y 是 x 的右儿子,且 y 没有左儿子。从而保证 y 在中序遍历中是 x 的后一个。
    if (Find(y_) != x_ || fa[y_] != x_ || son[y_][0]) return ;
    fa[y_] = son[x_][1] = 0; //断绝父子关系
    Pushup(x_);
  }
  void Modify(int x_, int y_, int val_, int type) { //路径修改
    Split(x_, y_); //给路径构成的 Splay 打标记
    if (!type) PushPlus(y_, val_);
    else PushProd(y_, val_);
  }
  LL Query(int x_, int y_) { //路径查询
    Split(x_, y_); //返回路径构成的 Splay 的信息
    return sum[y_];
  }
}

复杂度

上述 SplayAccess 操作的复杂度都是均摊 \(O(\log n)\) 的,可得其它所有操作的复杂度都是均摊 \(O(\log n)\) 的。

证明见:杨哲《QTREE 解法的一些研究》

小技巧

一次 Find 操作中进行了 1 次 Access 和 两次 Splay,并进行了一次 Splay 上的向下查询操作。这使得此操作复杂度较高。

在某些不带删除的题目中可以直接使用并查集维护树根,避免调用 Find 函数。

在保证加删边合法时(例如本题),就不需要在 LinkCut 操作中判断是否连通了。两个函数可改写为下述简单形式。同时避免了 Find 函数的调用,降低了复杂度。

void Link(int x_, int y_) {
  MakeRoot(x_);
  fa[x_] = y_;
}
void Cut(int x_, int y_) {
  Split(x_, y_); 
  fa[x_] = son[y_][0] = 0; //在 Splay 中 x 和 y 有直接连边,x 在中序遍历中是 y 的前一个,Split 后 Splay 的根是 y,则 x 一定是 y 的左儿子。
  Pushup(y_);
}

例题

我相信有能力学习 LCT 的读者并不需要过多对代码的解释了= =

「SDOI2008」洞穴勘测

维护连通性。

给定 \(n\) 个点,给定 \(m\) 次操作:

  1. 断开一条边,保证断开的边存在。
  2. 连接一条边,保证操作后是一片森林。
  3. 查询两点的连通性。

\(1\le n\le 10^4\)\(1\le m\le 2\times 10^5\)
1S,128MB。

板题,又保证操作合法,套板子即可。

维护树链信息。

给定 \(n\) 个点以及每个点的权值,给定 \(m\) 次操作:

  1. 询问路径上的点的权值异或和。
  2. 单点权值修改。
  3. 断开一条边,若边不存在则跳过。
  4. 连接一条边,若两点已连通则跳过。

\(1\le n\le 10^5\)\(1\le m\le 3\times 10^5\)
2S,128MB。

保证操作后仍是森林,Splay 维护子树节点异或和即可。注意判断操作不合法。可以用如下方法简单实现单点修改:

void Modify(int now_, int val_) {
  Splay(now_); //先转到根,修改后不影响其它子树的 xor 和
  val[now_] = val_;
  Pushup(now_);
}

「HNOI2010」弹飞绵羊

模型的建立。

下述题意是抽象过的,建议先阅读原题面。

给定一长度为 \(n\) 的正整数数列 \(k\)。对于点 \(1\sim n\),从点 \(i\) 向点 \(i + k_i\) 连有向边,若 \(i + k_i >n\) 不连边。
给定 \(m\) 次操作:

  1. 给定点 \(i\),查询以它为起点的有向路径上点的数量。
  2. 给定参数 \(j,k'\),表示将 \(k_j\) 修改为 \(k'\)

\(1\le n\le 2\times 10^5\)\(1\le m\le 10^5\)
1S,128MB。

考虑建立虚拟节点 \(n + 1\)。对于节点 \(i\),若满足 \(i + k_i>n\),则令 \(i\) 连向 \(n +1\)。发现每个节点只会向编号比它大的一个节点连一条边,构成了一个树状结构。
在这棵树上,询问的答案即为 \(n+1\)\(i\) 链的长度 \(- 1\)。修改相当于动态删边加边操作。Splay 维护子树大小即可。总复杂度 \(O(m\log n)\) 级别。

bzoj 4998 星球联盟

加边,维护边双连通分量。

时空限制是在 Mina! 看的 = =

给定一 \(n\) 个点 \(m\) 条边的无向图,给定 \(q\) 次操作。
每次操作加入一条边,查询加入这条边后这条边的两端点是否在一个边双连通分量中,若在则输出该分量内节点个数。
\(1< n,m,q< 2\times 10^5\),。
1S,256MB。

无向图边双缩点后一定是一棵树,考虑使用 LCT 维护加边操作。

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

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

并查集中维护 size。每次加边后考察两点是否在同一个边双内,若在则输出对应祖先的 size。

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

//知识点:LCT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
//=============================================================
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], siz[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_;
      siz[y_] += siz[x_];
    }
  }
}
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_) {
    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_);
  }
  bool Link(int x_, int y_) {
    x_ = UF::Find(x_), y_ = UF::Find(y_);
    if (x_ == y_) return true;
    MakeRoot(x_);
    if (Find(y_) != x_) {
      fa[x_] = y_;
      return false;
    }
    Delete(son[x_][1], x_); //合并原节点
    son[x_][1] = 0; //x 即合并后的得到的代表边双的节点。将它的儿子置零。
    Pushup(x_);
    return true;
  }
  int Query(int x_, int y_) {
    x_ = UF::Find(x_), y_ = UF::Find(y_);
    Split(x_, y_);
    return siz[y_] - 1;
  }
}
//=============================================================
int main() { 
  int n = read(), m = read(), p = read();
  for (int i = 1; i <= n; ++ i) {
    UF::fa[i] = i;
    LCT::siz[i] = UF::siz[i] = 1;
  }
  for (int i = 1; i <= m; ++ i) {
    int u_ = read(), v_ = read();
    LCT::Link(u_, v_);
  }
  for (int i = 1; i <= p; ++ i) {
    int u_ = read(), v_ = read();
    if (LCT::Link(u_, v_)) {
      printf("%d\n", UF::siz[UF::Find(v_)]);
    } else{
      printf("No\n");
    }
  }
  return 0; 
}

「AHOI2005」航线规划

加边,维护边双连通分量。

给定一 \(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。

先离线反向操作,将删边转化为加边。
容易发现查询的答案即原图边双连通分量缩点后指定两点间的边数,同上题使用并查集维护节点所在边双。查询时取出指定路径,答案即路径上的点数 \(-1\),维护 Splay 子树大小即可。
总复杂度 \(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(son[x_][1], 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; 
}

bzoj 2959 长跑

加边,维护边双连通分量。

给定一 \(n\) 个点的无向图,点有点权,初始图中没有边。给定 \(m\) 次操作:

  1. 连接一条无向边。
  2. 单点权值修改。
  3. 给定节点 \(u,v\),要求把所有边定向,最大化从 \(u\)\(v\) 的所有路径中,经过的点权值和的最大值(经过多次权值也只计算一次),输出该最大值。

\(n\le 1.5 \times 10^5\)\(1\le m\le 5n\),点权非负。
1S,256MB。

点权非负,显然应该尽量增加经过的点数。
考虑这个查询操作。对于一个边双连通分量,显然可通过构造使得可以在边双里绕圈圈,从而令每个节点都可以被经过并获得其权值,且可以从任意一个节点离开边双。

考虑对原图进行边双缩点,新点的权值为内部所有节点的权值和,查询的答案即两点路径上所有节点的权值和,使用 LCT 维护加边、点权修改即可。
总复杂度 \(O(m\log n)\)

//知识点:LCT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int origin[kN];
//=============================================================
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], val[kMaxNode];
  LL sum[kMaxNode];
  bool tagrev[kMaxNode];
  void Pushup(int now_) {
    now_ = UF::Find(now_);
    sum[now_] = sum[ls] + sum[rs] + val[now_];
  }
  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_) {
    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::fa[now_] = fa_;
    val[fa_] += val[now_];
    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(son[x_][1], x_); //合并原节点
    son[x_][1] = 0; //x 即合并后的得到的代表边双的节点。将它的儿子置零。
    Pushup(x_);
  }
  void Modify(int x_, int val_) { //单点修改
    Splay(UF::Find(x_));
    val[UF::Find(x_)] -= origin[x_];
    origin[x_] = val_;
    val[UF::Find(x_)] += origin[x_];
    Pushup(UF::Find(x_));
  }
  LL Query(int x_, int y_) { //查询路径权值和
    x_ = UF::Find(x_), y_ = UF::Find(y_);
    MakeRoot(x_);
    if (Find(y_) != x_) return -1; //不连通
    Split(x_, y_);
    return sum[y_];
  }
}
//=============================================================
int main() { 
  int n = read(), m = read();
  for (int i = 1; i <= n; ++ i) {
    UF::fa[i] = i;
    origin[i] = LCT::val[i] = LCT::sum[i] = read();
  }
  for (int i = 1; i <= m; ++ i) {
    int opt = read(), x = read(), y = read();
    if (opt == 1) LCT::Link(x, y);
    if (opt == 2) LCT::Modify(x, y);
    if (opt == 3) printf("%lld\n", LCT::Query(x, y));
  }
  return 0; 
}

P4234 最小差值生成树

LCT 维护生成树。

给定一 \(n\) 个点,\(m\) 条边的无向图,求边权最大值与最小值的差值最小的生成树,输出最小的差值。图可能存在自环。
\(1\le n\le 5\times 10^4\)\(1\le m\le 2\times 10^5\)\(1\le w\le 10^4\)
3S,256MB。

考虑按照权值升序枚举边,并加入生成树中,以固定边权值最大的边。生成树中权值最小的边的权值应尽可能小,考虑使用 LCT 动态维护生成树中最小的边。
若加入一条边 \((u,v)\) 后图上会产生环,为保证树的形态,则需删掉原树中路径 \((u,v)\) 中任意一条边。显然应贪心地删去权值最小的边,LCT 维护最小边权即可。只要加入新边后构成生成树就统计贡献。

由于树的形态不定,边权并不好直接维护,考虑边转点,对每条边都新建一个虚拟节点并储存边权。
总复杂度 \(O(m\log n)\) 级别。

//知识点:LCT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <set>
#define LL long long
const int kN = 5e5 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
struct Edge {
  int u, v, w;
} e[kN];
int ans = kInf;
std::multiset <int> edge;
//=============================================================
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;
}
bool CMP(Edge fir_, Edge sec_) {
  return fir_.w < sec_.w;
}
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], val[kMaxNode];
  int minval[kMaxNode], node[kMaxNode];
  bool tagrev[kMaxNode];
  void Pushup(int now_) {
    minval[now_] = val[now_], node[now_] = now_;
    if (ls && minval[ls] < minval[now_]) {
      minval[now_] = minval[ls], node[now_] = node[ls];
    }
    if (rs && minval[rs] < minval[now_]) {
      minval[now_] = minval[rs], node[now_] = node[rs];
    }
  }
  void PushReverse(int now_) { 
    if (!now_) return;
    std::swap(ls, rs);
    tagrev[now_] ^= 1;
  }
  void Pushdown(int now_) {
    if (tagrev[now_]) PushReverse(ls), PushReverse(rs);
    tagrev[now_] = 0;
  }
  bool IsRoot(int now_) { 
    return son[f][0] != now_ && son[f][1] != now_;
  }
  bool WhichSon(int now_) {
    return son[f][1] == now_;
  }
  void Rotate(int now_) {
    int fa_ = f, w = WhichSon(now_);
    if (!IsRoot(f)) son[fa[f]][WhichSon(f)] = now_;
    f = fa[f];

    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_) { 
    if (!IsRoot(now_)) Update(f);
    Pushdown(now_);
  }
  void Splay(int now_) {
    Update(now_);
    for (; !IsRoot(now_); Rotate(now_)) {
      if (!IsRoot(f)) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_);
    }
  }
  void Access(int now_) {
    for (int last_ = 0; now_; last_ = now_, now_ = f) {
      Splay(now_), rs = last_;
      Pushup(now_);
    }
  }
  void MakeRoot(int now_) {
    Access(now_);
    Splay(now_);
    PushReverse(now_);
  }
  int Find(int now_) {
    Access(now_); 
    Splay(now_);
    while (ls) Pushdown(now_), now_ = ls;
    
    Splay(now_);
    return now_; 
  }
  void Split(int x_, int y_) {
    MakeRoot(x_);
    Access(y_);
    Splay(y_);
  }
  void Link(int x_, int y_) {
    MakeRoot(x_);
    if (Find(y_) != x_) fa[x_] = y_;
  }
  void Cut(int x_, int y_) {
    MakeRoot(x_);
    if (Find(y_) != x_ || fa[y_] != x_ || son[y_][0]) return ;
    fa[y_] = son[x_][1] = 0;
    Pushup(x_);
  }
}
//=============================================================
int main() { 
  int n = read(), m = read();
  for (int i = 1; i <= n; ++ i) {
    LCT::val[i] = LCT::minval[i] = kInf;
    LCT::node[i] = i;
  }
  for (int i = 1; i <= m; ++ i) e[i] = (Edge) {read(), read(), read()};
  std::sort(e + 1, e + m + 1, CMP);
  for (int i = 1, tot = 0; i <= m; ++ i) {
    int u_ = e[i].u, v_ = e[i].v, w_ = e[i].w;
    if (u_ == v_) continue;
    LCT::val[n + i] = LCT::minval[n + i] = w_;
    LCT::node[n + i] = n + i;
    if (LCT::Find(u_) != LCT::Find(v_)) { //加边后没有环
      ++ tot;
    } else { //加边后产生环
      LCT::Split(u_, v_);
      int minval = LCT::minval[v_], node = LCT::node[v_]; //找到最小边
      edge.erase(edge.find(minval)); //削除
      LCT::Cut(e[node - n].u, node), LCT::Cut(e[node - n].v, node);
    }
    LCT::Link(u_, n + i), LCT::Link(v_, n + i); //连边
    if (tot == n - 1) { //同级生成树的贡献
      if (!edge.size()) Chkmin(ans, e[i].w);
      else Chkmin(ans, e[i].w - (*edge.begin()));
    }
    edge.insert(w_);
  }
  printf("%d\n", ans);
  return 0; 
}

「NOI2014」魔法森林

LCT 维护生成树

给定一 \(n\) 个节点 \(m\) 条边的无向图,每条边有两个属性 \((a_i, b_i)\)
对于一棵生成树 \(T\),最小化并输出:

\[\left(\max_{e_i\in T} a_i\right) + \left(\max_{e_i\in T} b_i\right) \]

\(2\le n\le 5\times 10^4\)\(0\le m\le 10^5\)\(1\le a_i,b_i\le 5\times 10^4\)
1S,256MB。

二元最小生成树问题。

考虑把所有边按 \(a_i\) 升序排序,按顺序枚举加入生成树中,固定树中各边 \(a_i\) 的最大值。同上题的套路,先边转点,若加入新边后产生环,删去原路径上 \(b\) 最大的边以最小化 \(b\)。只要加入新边后构成生成树就 Split(1,n) 来统计贡献。

LCT 分别维护两个属性的最大值即可。总复杂度 \(O(m\log n)\)
加入的边不一定影响 \(1\rightarrow n\),为什么这样可以保证正确性?可以发现上面实际上就是在模拟 Kruskal 构建最小瓶颈树的过程,结合 Kruskal 的性质可知这样是正确的。

//知识点:LCT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 5e5 + 10;
const int kInf = 2e9 + 2077;
//=============================================================
struct Edge {
  int u, v, w1, w2;
} e[kN];
int n, m, ans = kInf, fa[kN];
//=============================================================
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;
}
bool CMP(Edge fir_, Edge sec_) {
  return fir_.w1 < sec_.w1;
}
int Find(int 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], vala[kMaxNode], valb[kMaxNode];
  int mxa[kMaxNode], posa[kMaxNode], mxb[kMaxNode], posb[kMaxNode];
  bool tagrev[kMaxNode];
  void Init(int now_, int vala_, int valb_) {
    vala[now_] = mxa[now_] = vala_;
    valb[now_] = mxb[now_] = valb_;
    posa[now_] = posb[now_] = now_;
  }
  void Pushup(int now_) {
    mxa[now_] = vala[now_], mxb[now_] = valb[now_];
    posa[now_] = posb[now_] = now_;
    if (ls && mxa[ls] > mxa[now_]) {
      mxa[now_] = mxa[ls], posa[now_] = posa[ls];
    }
    if (rs && mxa[rs] > mxa[now_]) {
      mxa[now_] = mxa[rs], posa[now_] = posa[rs];
    }
    if (ls && mxb[ls] > mxb[now_]) {
      mxb[now_] = mxb[ls], posb[now_] = posb[ls];
    }
    if (rs && mxb[rs] > mxb[now_]) {
      mxb[now_] = mxb[rs], posb[now_] = posb[rs];
    }
  }
  void PushReverse(int now_) { 
    if (!now_) return;
    std::swap(ls, rs);
    tagrev[now_] ^= 1;
  }
  void Pushdown(int now_) {
    if (tagrev[now_]) PushReverse(ls), PushReverse(rs);
    tagrev[now_] = 0;
  }
  bool IsRoot(int now_) { 
    return son[f][0] != now_ && son[f][1] != now_;
  }
  bool WhichSon(int now_) {
    return son[f][1] == now_;
  }
  void Rotate(int now_) {
    int fa_ = f, w = WhichSon(now_);
    if (!IsRoot(f)) son[fa[f]][WhichSon(f)] = now_;
    f = fa[f];

    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_) { 
    if (!IsRoot(now_)) Update(f);
    Pushdown(now_);
  }
  void Splay(int now_) {
    Update(now_);
    for (; !IsRoot(now_); Rotate(now_)) {
      if (!IsRoot(f)) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_);
    }
  }
  void Access(int now_) {
    for (int last_ = 0; now_; last_ = now_, now_ = f) {
      Splay(now_), rs = last_;
      Pushup(now_);
    }
  }
  void MakeRoot(int now_) {
    Access(now_);
    Splay(now_);
    PushReverse(now_);
  }
  int Find(int now_) {
    Access(now_); 
    Splay(now_);
    while (ls) Pushdown(now_), now_ = ls;
    
    Splay(now_);
    return now_; 
  }
  void Split(int x_, int y_) {
    MakeRoot(x_);
    Access(y_);
    Splay(y_);
  }
  void Link(int x_, int y_) {
    MakeRoot(x_);
    if (Find(y_) != x_) fa[x_] = y_;
  }
  void Cut(int x_, int y_) {
    MakeRoot(x_);
    if (Find(y_) != x_ || fa[y_] != x_ || son[y_][0]) return ;
    fa[y_] = son[x_][1] = 0;
    Pushup(x_);
  }
  int Query(int x_, int y_) {
    Split(x_, y_);
    return mxa[y_] + mxb[y_];
  }
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) fa[i] = i;
  for (int i = 1; i <= m; ++ i) {
    e[i].u = read(), e[i].v = read();
    e[i].w1 = read(), e[i].w2 = read();
  }
  std :: sort(e + 1, e + m + 1, CMP);

  for (int i = 1; i <= m; ++ i) {
    if (e[i].u == e[i].v) continue;
    int u_ = e[i].u, v_ = e[i].v, w1_ = e[i].w1, w2_ = e[i].w2;
    LCT::Init(i + n, w1_, w2_);
    
    if (Find(u_) != Find(v_)) {
      Union(u_, v_);
      LCT::Link(u_, i + n), LCT::Link(v_, i + n);
    } else {
      LCT::Split(u_, v_);
      int mxb = LCT::mxb[v_], posb = LCT::posb[v_];
      if (mxb <= w2_) continue;
      LCT::Cut(e[posb - n].u, posb), LCT::Cut(e[posb - n].v, posb);
      LCT::Link(i + n, u_), LCT::Link(i + n, v_);
    }
    if (Find(1) == Find(n)) Chkmin(ans, LCT::Query(1, n));
  }
  printf("%d\n", ans == kInf ? -1 : ans);
	return 0;
}
//*我写 kruscal 骗分,好吗?
//评测机:*不!
//5pts

写在最后

鸣谢

Link Cut Tree - OI Wiki
杨哲《QTREE 解法的一些研究》
Link-Cut-Tree - PoPoQQQ
Link-Cut Tree(LCT)&TopTree讲解 - The_Virtuoso

posted @ 2021-01-22 21:02  Luckyblock  阅读(185)  评论(4编辑  收藏  举报