线段树进阶
线段树分治(时间线段树)
线段树分治是一种离线的算法,按时间分治。常用于处理每个操作有一定的生效时间(或者每个查询限制一段时间)的题目。
其本质是钦定良好的顺序来得出答案,使得执行操作的次数最少。
而对于具有类似思想的 trick 有:对于一些图论问题,可以将操作离线,然后对于每一个操作将其加到对应的节点上去,然后根据一定的次序进行维护。(最早见到是在 NOIP2024 前的某一场模拟赛的 C 中见到的,那之后也还有见到一道运用了类似思想的题)
适用于支持插入而不支持删除的数据结构,如线性基,并查集等,将删除操作转化为撤销操作。而撤销操作只需开个栈记录下每次操作的自由度即可,然后再需要时弹栈撤销。
注意每次处理完叶子节点的询问之后要撤销叶子节点的操作。
注意到线段树有良好的分治结构,对于每一段时间我们可以将其分割为至多 \(\log T\) 个节点(其中 \(T\) 是指总时间)。对于时刻 \(t\) 所生效的操作就可以按照中序遍历的顺序遍历到维护时间段 \([t,t]\) 的节点所得到所需的操作,这样每个操作也就只会被加入删除一次。每次遍历到该节点就执行操作,出节点就回溯弹栈撤销。
时间复杂度 \(O(Tk \log T)\),其中 \(k\) 是单次操作的时间复杂度。
首先可以用带撤销扩展域并查集(也可以写可持久化并查集,不过应该没人写可持久化并查集吧)维护二分图判定,注意并查集不可以路径压缩,而是要按秩合并,否则不支持撤销操作,注意到每次操作的时间复杂度是 \(O(\log n)\) 的,因此总时间复杂度是 \(O(m \log k \log n)\).
其中有一个二分图的性质:一个非二分图加一条边一定不会是二分图。
#include <vector>
#include <cstdio>
#include <algorithm>
const int N=1e5+10;
const int M=2e5+10;
const int K=1e5+10;
int n,m,k;
struct Seg_Divide {
bool chk[K<<2];
int fa[N<<1],siz[N<<1];
std::vector<std::pair<int,int>> val[K<<2];
#define ls p<<1
#define rs p<<1|1
void modify(int l,int r,int L,int R,int p,std::pair<int,int> pii) {
if(l>r) return;
if(l<=L&&R<=r) {val[p].push_back(pii);return;}
int mid=L+R>>1;
if(l<=mid) modify(l,r,L,mid,ls,pii);
if(mid<r) modify(l,r,mid+1,R,rs,pii);
}
void init() {
chk[1]=true;
for(int i=1;i<=(n<<1);i++) {fa[i]=i;siz[i]=1;}
}
int f(int x) {
return fa[x]==x?x:f(fa[x]);
}
void undo(std::vector<std::pair<int,int>> &x) {
for(auto it:x) {
siz[it.first]-=siz[it.second];
fa[it.second]=it.second;
}
}
void query(int l,int r,int p) {
std::vector<std::pair<int,int>> upd;
if(chk[p]) {
for(auto it:val[p]) {
int u1=it.first,v1=it.second;
int u2=u1+n,v2=v1+n;
int fu1=f(u1),fv1=f(v1);
int fu2=f(u2),fv2=f(v2);
if(fu1==fv1 || fu2==fv2) {chk[p]=false;break;}
if(siz[fu1]<siz[fv2]) std::swap(fu1,fv2);
if(siz[fu2]<siz[fv1]) std::swap(fu2,fv1);
upd.push_back(std::make_pair(fu1,fv2));
upd.push_back(std::make_pair(fu2,fv1));
siz[fu1]+=siz[fv2];siz[fu2]+=siz[fv1];
fa[fv2]=fu1;fa[fv1]=fu2;
}
}
if(l==r) {
puts(chk[p]?"Yes":"No");
undo(upd); // 注意叶子节点处的撤销
return;
}
chk[ls]=chk[rs]=chk[p];
int mid=l+r>>1;
query(l,mid,ls);query(mid+1,r,rs);undo(upd);
}
#undef ls
#undef rs
}T;
int main() {
scanf("%d%d%d",&n,&m,&k);
T.init();
for(int i=1,x=0,y=0,l=0,r=0;i<=m;i++) {
scanf("%d%d%d%d",&x,&y,&l,&r);
T.modify(l+1,r,1,k,1,std::make_pair(x,y)); // 时刻转为时间段
}
T.query(1,k,1);
return 0;
}
参考文章
[1] 线段树的高级用法 https://www.cnblogs.com/alex-wei/p/segment_tree_yyds.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】