「AHOI2005」航线规划
知识点:LCT,维护加边的边双连通分量
原题面:Luogu。
LCT 相关内容可以阅读:「笔记」Link Cut Tree。
简述
给定一 个点 条边的无向图,给定 次操作:
- 断开一条边,保证断开的边存在。
- 查询给定两点间所有路径的必经边的数量。
,,。
1S,128MB。
分析
先离线反向操作,将删边转化为加边。
容易发现查询的答案即原图边双缩点后指定两点间的边数。无向图边双缩点后一定是一棵树,考虑使用 LCT 维护加边操作。
具体地,使用并查集维护每个点所在的边双(注意不是连通性)。加边时分类讨论。
- 若两点已经在一个边双中,跳过操作。
- 两点不连通,直接连边即可。
- 两点已连通,考虑把两点路径上所有点取出,把它们合并成一个点。新点代表它们所在的边双。
查询时取出指定路径,答案即路径上的点数 ,维护 Splay 子树大小即可。
具体实现上:对于需要合并成一个点的路径,将该路径取出后遍历其中所有点,并在并查集中把它们合并。之后每次需要调用一个 LCT 节点时,都找到该节点在并查集中的祖先,然后调用它的祖先。由于事先 Access(u,v)
了,这样做显然不会破坏 LCT 中原有的各 Splay 的父子关系,可以保证正确性。
考虑复杂度。每个原图中的节点都只会被合并一次,合并一个节点均摊 。则总复杂度为常数飞天的 级别。
上述做法通用性较强。对于此题来说,还有另外一种解法。
同样离线反向操作,先进行边双缩点,在新图上考虑边转点。将代表边的点的权值赋为 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; }
作者@Luckyblock,转载请声明出处。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 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】