线段树进阶

线段树分治(时间线段树)

线段树分治是一种离线的算法,按时间分治。常用于处理每个操作有一定的生效时间(或者每个查询限制一段时间)的题目。

其本质是钦定良好的顺序来得出答案,使得执行操作的次数最少。

而对于具有类似思想的 trick 有:对于一些图论问题,可以将操作离线,然后对于每一个操作将其加到对应的节点上去,然后根据一定的次序进行维护。(最早见到是在 NOIP2024 前的某一场模拟赛的 C 中见到的,那之后也还有见到一道运用了类似思想的题)

适用于支持插入而不支持删除的数据结构,如线性基,并查集等,将删除操作转化为撤销操作。而撤销操作只需开个栈记录下每次操作的自由度即可,然后再需要时弹栈撤销。

注意每次处理完叶子节点的询问之后要撤销叶子节点的操作。

注意到线段树有良好的分治结构,对于每一段时间我们可以将其分割为至多 \(\log T\) 个节点(其中 \(T\) 是指总时间)。对于时刻 \(t\) 所生效的操作就可以按照中序遍历的顺序遍历到维护时间段 \([t,t]\) 的节点所得到所需的操作,这样每个操作也就只会被加入删除一次。每次遍历到该节点就执行操作,出节点就回溯弹栈撤销。

时间复杂度 \(O(Tk \log T)\),其中 \(k\) 是单次操作的时间复杂度。

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

首先可以用带撤销扩展域并查集(也可以写可持久化并查集,不过应该没人写可持久化并查集吧)维护二分图判定,注意并查集不可以路径压缩,而是要按秩合并,否则不支持撤销操作,注意到每次操作的时间复杂度是 \(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

posted @   ldh081122  阅读(4)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示