线段树分治
好耶有我最喜欢的并查集!
线段树与离线询问结合的问题在 OI 领域也有出现。这种技巧又被称为线段树分治。
假如你需要维护一些信息,这些信息会在某一个时间段内出现,要求在离线的前提下回答某一个时刻的信息并,则可以考虑使用线段树分治的技巧。
实际上线段树分治常有以下用途:
1. 用原本不支持删除但是支持撤销的数据结构来模拟删除操作.如朴素的并查集无法高效支持删边操作。
2. 不同属性的数据分别计算。如需要求出除了某一种颜色外,其他颜色数据的答案。
————摘自 OI-Wiki
看不懂没事,下面会讲清楚的。
从模板开始
有 \(n\) 个点,需要支持以下操作:
- 连一条边
- 删一条边
- 判断图的连通性
step 1 简化
考虑没有删除的话,应该怎么做。
我们可以用并查集,判断图的连通性显然是轻而易举。
step 2 如何加删除操作
还是用并查集判断连通性,但是删除这个东西怎么办呢?
我们发现连一条边+删一条边可以转化为这一条边在某一时间段内出现。
我们就能建一棵线段树(主角登场)维护时刻,什么意思呢?

这是一棵线段树,每一个点上开个 vector 存边。如果我们要加一条在时刻 \([2,5]\) 出现连接 \((3,6)\) 的边,那么就把它挂在染成绿色的节点上。

这里我们使用标记永久化的思想,如果 pushdown,那么 TLE,MLE 等着你。
停停停!说这么多,这是要干什么?
这样做是因为,我们从根节点往下走,走到叶子节点时,形成的图刚好是那个时刻的图,而且退出刚好就是撤销操作!
这样就可以把删除操作改为撤销操作了qwq。
别告诉我不会并查集的撤销,事实上搞个栈就行了。
struct dsu{//并查集(按秩合并)
int f[N],siz[N],top1,top2;
pii st1[M],st2[M];
void init(){
for(int i=1;i<=n;i++) f[i]=i,siz[i]=1;
}
int find(int i){
if(f[i]==i) return i;
return find(f[i]);
}
void merge(int x,int y){
int tx=find(x),ty=find(y);
if(tx==ty) return;
if(siz[tx]<siz[ty]) swap(tx,ty);
st1[++top1]=make_pair(tx,siz[tx]);//用于撤销
st2[++top2]=make_pair(ty,fa[ty]);
siz[tx]=max(siz[tx],siz[ty]+1);
f[ty]=tx;
}
void del(){//撤销
pii u=st1[top1--],v=st2[top2--];
siz[u.first]=u.second;
f[v.first]=v.second;
}
}a;
void addedge(int x,int L,int R,int l,int r,int u,int v){
if(l<=L&&R<=r){
tr[x].push_back(make_pair(u,v));
return;
}
int mid=(L+R)>>1;
if(mid>=l) addedge(ls,L,mid,l,r,u,v);
if(mid<r) addedge(rs,mid+1,R,l,r,u,v);
}
void query(int x,int L,int R){
int top=a.top1;
for(auto j:tr[x]){
int x=j.first,y=j.second;
a.merge(x,y);
}
if(a.siz[a.find(1)]==n){//已经联通就不用往下了
for(int i=L;i<=R;i++) can[i]=true;
while(a.top1>top) a.del();
return;
}
if(L==R){
while(a.top1>top) a.del();
return;
}
int mid=(L+R)>>1;
query(ls,L,mid);
query(rs,mid+1,R);
while(a.top1>top) a.del();
}
这就是线段树与离线询问的结合——线段树分治的第一种应用。
题目
我做过的题目不多,如果想多练练手,可以自己搜一些线段树分治的题目。
水题
第二种情况
还是从例题开始
一张图,边有边权。求边权集合 \(mex\) 最小的生成树。
思路
大多数人看到这题根本不会把它和线段树分治联系上,所以说这题是套路题。
我们按边权建出线段树,然后让边权为 \(i\) 的边挂在 \([1,i-1]\) 和 \([i+1,MAXW]\) 时间段内,如果 \(i \le mex\{w_1,w_2,\dots,w_m\}\),在 \(i\) 时刻边权集合的 \(mex\) 就是 \(i\),直接判断是否联通即可。
#include <bits/stdc++.h>
#define getchar getchar_unlocked
#define pii pair<int,int>
#define ls (x<<1)
#define rs (x<<1|1)
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
const int N=1e6+10,MAXW=1e5,M=2e6+10;
vector<pii> tr[MAXW<<2];
int n,m,can[N];
struct dsu{//并查集(启发式合并)
int f[N],siz[N],top1;
pii st1[M<<1];
void init(){
for(int i=1;i<=n;i++) f[i]=i,siz[i]=1;
}
int find(int i){
if(f[i]==i) return i;
return find(f[i]);
}
void merge(int x,int y){
int tx=find(x),ty=find(y);
if(tx==ty) return;
if(siz[tx]<siz[ty]) swap(tx,ty);
st1[++top1]=make_pair(tx,ty);//用于撤销
siz[tx]=siz[tx]+siz[ty];
f[ty]=tx;
}
void del(){//撤销
pii u=st1[top1];top1--;
siz[u.first]-=siz[u.second];
f[u.second]=u.second;
}
}a;
void addedge(int x,int L,int R,int l,int r,int u,int v){
if(l<=L&&R<=r){
tr[x].push_back(make_pair(u,v));
return;
}
int mid=(L+R)>>1;
if(mid>=l) addedge(ls,L,mid,l,r,u,v);
if(mid<r) addedge(rs,mid+1,R,l,r,u,v);
}
void query(int x,int L,int R){
int top=a.top1;
for(auto j:tr[x]){
int x=j.first,y=j.second;
a.merge(x,y);
}
if(a.siz[a.find(1)]==n){//已经联通就不用往下了,而且因为线段树递归顺序,此时 L 就是答案
cout<<L;
exit(0);
}
if(L==R){
while(a.top1>top) a.del();
return;
}
int mid=(L+R)>>1;
query(ls,L,mid);
query(rs,mid+1,R);
while(a.top1>top) a.del();
}
int main(){
n=read(),m=read();
a.init();
for(int i=1;i<=m;i++){
int u,v,w;u=read(),v=read(),w=read();
if(w>0) addedge(1,0,MAXW+1,0,w-1,u,v);
if(w<MAXW) addedge(1,0,MAXW+1,w+1,MAXW+1,u,v);
}
query(1,0,MAXW+1);
cout<<MAXW+1;//因为保证全图联通,所以一定有解,1~MAXW 中没有解,那么必然是 MAXW+1
return 0;
}
发现没有?我们把不同边权视作不同颜色,这个情况就是上边说的第二种情况。
- 不同属性的数据分别计算。如需要求出除了某一种颜色外,其他颜色数据的答案。
题目
P3854 [TJOI2008] 通讯网破坏
P14532 [RMI 2018] 颜色 / Colors
再来
我们直接把“第 \(i\) 个塔能接受的频率范围是 \(l_i\) 到 \(r_i\)”转化为“第 \(i\) 个塔只在 \(l_i\) 到 \(r_i\) 时刻内出现”,那么在线段树分治中,递归到叶子节点,里面的塔都能承受相同频率,互相连通的塔就能互传信息了。
但是如何统计答案?如果我们直接在叶子节点给所有能与 \(1\) 号塔连通的都扫一遍,完全是 TLE 的节奏。
思考后再打开我
我们使用 kruskal 重构树的思想,当两点连接,则建出新的节点。

然后在叶子节点给“重构树”上的根打一个标记。

然后在撤销时把标记下传。

最后所有能连通的塔都 \(O(1)\) 拥有了 \(+1\) 标记。
由此可见,线段树分治可以与其它许多思想结合,还是很强大的。
还有第三关
时间轴上搞线段树分治,然后李超线段树做完了。
自己可以练练码力,我不放代码了绝对不是因为我懒。
结语
算法之路无穷无尽,哪怕是最最普通的算法也有独属于它的魅力,这是 OIer 的浪漫。
愿你在自己的路上走出光彩。

浙公网安备 33010602011771号