线段树分治

好耶有我最喜欢的并查集!

线段树与离线询问结合的问题在 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();
}

这就是线段树与离线询问的结合——线段树分治的第一种应用。

题目

我做过的题目不多,如果想多练练手,可以自己搜一些线段树分治的题目。

P5787【模板】线段树分治 / 二分图

为什么这题是模板题???

hint1

看到 在 l 时刻出现 r 时刻消失 怎么办?

hint2

如何快速判断二分图?

P3950 部落冲突

判断是否连通是简单的。

hint1

这题是 在 l 时刻消失 r 时刻出现 怎么办?

水题

P2147 [SDOI2008] 洞穴勘测

第二种情况

还是从例题开始

P5631 最小mex生成树

一张图,边有边权。求边权集合 \(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;
}

发现没有?我们把不同边权视作不同颜色,这个情况就是上边说的第二种情况。

  1. 不同属性的数据分别计算。如需要求出除了某一种颜色外,其他颜色数据的答案。

题目

P3854 [TJOI2008] 通讯网破坏
P14532 [RMI 2018] 颜色 / Colors

再来

CF1814F Communication Towers

我们直接把“第 \(i\) 个塔能接受的频率范围是 \(l_i\) 到 \(r_i\)”转化为“第 \(i\) 个塔只在 \(l_i\) 到 \(r_i\) 时刻内出现”,那么在线段树分治中,递归到叶子节点,里面的塔都能承受相同频率,互相连通的塔就能互传信息了。

但是如何统计答案?如果我们直接在叶子节点给所有能与 \(1\) 号塔连通的都扫一遍,完全是 TLE 的节奏。

思考后再打开我

我们使用 kruskal 重构树的思想,当两点连接,则建出新的节点。

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

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

最后所有能连通的塔都 \(O(1)\) 拥有了 \(+1\) 标记。

由此可见,线段树分治可以与其它许多思想结合,还是很强大的。

还有第三关

CF678F Lena and Queries

时间轴上搞线段树分治,然后李超线段树做完了。
自己可以练练码力,我不放代码了绝对不是因为我懒。

结语

算法之路无穷无尽,哪怕是最最普通的算法也有独属于它的魅力,这是 OIer 的浪漫。

愿你在自己的路上走出光彩。

posted @ 2026-10-21 14:48  CAN_NOT_AC  阅读(12)  评论(0)    收藏  举报