「AHOI2005」航线规划

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

原题面:Luogu

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

简述

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

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

1<n<3×1041<m<1050q4×104
1S,128MB。

分析

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

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

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

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

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


上述做法通用性较强。对于此题来说,还有另外一种解法。
同样离线反向操作,先进行边双缩点,在新图上考虑边转点。将代表边的点的权值赋为 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 @   Luckyblock  阅读(76)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示