线段树优化建图

线段树优化建图

顾名思义,就是利用线段树建图,达到一些特殊的目的
常见的应用就是,对于一个点向一个区间连边,或者一个区间内的所有点向某一个点连边,直接连会TLE,那么就可以用到线段树优化建图

基本思想

先建一棵线段树。假如现在我们要从8号点向区间\([3,7]\)的所有点连一条权值为w的有向边
image

把区间\([3,7]\)拆成若干个线段树上的区间,分别与8号点单向边,如下图所示。其中黑色普通边的边权为0,粉色边的边权为w
image

这样,原本需要连\(n\)条边的操作现在只用连\(\log{(n)}\)条边,时间复杂度也就优化到了一个\(log\)
而对于区间向点连边的操作,与上面的操作类似,但所有边的方向都需要倒过来
image

那么如果两个操作同时出现怎么处理呢?
考虑同时建出两棵线段树,一棵正着建,另一棵反着建,初始把所有的叶子节点互相连一条无向边,边权为0,代表这些叶子节点实际上反映同一些点,也就是原本的若干个单独的点。剩下的操作都和上面一样
image

具体实现可以看一道例题

CF768B Legacy

线段树优化建图板子题。实际实现过程中,建树时利用a数组存储所有正向线段树的叶子节点的位置,由于初始化时把所有正反两向线段树的对应节点通过无向边连通了,所以这两棵线段树的叶子节点等效。对于操作一,直接把a数组记录的u,v的对应位置连边即可。对于操作二、三,把对应线段树上的区间对应的点向a数组对应位置连边,逻辑和modify相同。搜最短路时从反向树的叶子节点开始搜,因为所有线段树上连边都是单向边。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ls(p) p<<1
#define rs(p) p<<1|1
#define N 500000
#define inf 0x3f3f3f3f3f3f3f3f

/*
思路:线段树优化建图
发现题目中有点向区间连边和区间向点连边的神秘操作,考虑什么数据结构可以
得到这样的区间,于是想到线段树。对于点向区间连边的操作,由该点向线段树
上对应区间连边。为了保证连通性,线段树上父亲节点向儿子节点连边权为0的
单向边,可以保证不产生额外贡献。对于区间向点连边同理,把父亲连向儿子的
边改为儿子连向父亲的边即可。为了避免在线段树上跑出0环(父子之间各有一条
边权为0的边),需要正反两种操作建两棵线段树。而考虑到两棵线段树的叶子
节点与原节点一一对应,实质上是同样的节点,所以对两棵线段树上的叶子节点
之间一一连边权为0的双向边。线段树上连边时,和modify逻辑相同,只不过把
修改操作改为连边操作即可。最后跑一边最短路。 
*/

int n,m,s;
int op,u,v,l,r,w;

/*初始化图*/
struct segTree{
	int l,r;
}t[N*4+100];
struct edge{
	int to,nxt,w;
}e[N*4+100];
int a[N*4+100];
//记录所有线段树的叶子节点的位置,用于最后查询时快速获取 
int head[N*4+100],tot;
int dis[N*4+100];
bool vis[N*4+100];

void add(int u,int v,int w){
	e[++tot].to=v;
	e[tot].w=w;
	e[tot].nxt=head[u];
	head[u]=tot;
}

/*线段树模块*/
void build(int p,int l,int r){
	t[p].l=l,t[p].r=r;
	if(l==r){
		a[l]=p;
		return;
	}
	int mid=(l+r)>>1;
	//线段树父亲节点向子节点连便权为0的边 
	add(p,ls(p),0);
	add(p,rs(p),0);
	//第二棵线段树上子节点向父亲节点连边权为0的反边
	//这里提前空余出第一颗线段树的大小
	add((ls(p))+N,p+N,0);
	add((rs(p))+N,p+N,0);
	build(ls(p),l,mid);
	build(rs(p),mid+1,r);
	return; 
}

//线段树上连边
void connect(int p,int L,int R,int x,int w,int op){
	int l=t[p].l,r=t[p].r;
	if(L<=l&&r<=R){
		if(op) add(p+N,x,w);//三号操作,在反向线段树上寻找区间连边 
		else add(x,p,w);//二号操作,在正向线段树上寻找区间连边 
		return;
	}
	int mid=(l+r)>>1;
	if(L<=mid) connect(ls(p),L,R,x,w,op);
	if(R>mid) connect(rs(p),L,R,x,w,op);
	return;
}

/*最短路模块*/ 
struct dot{
	int u,d;
	bool operator <(const dot &a) const{
		return a.d<d;
	}
};

void dij(){
	priority_queue<dot> q;
	memset(dis,0x3f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	dis[s]=0;
	q.push({s,0});
	while(!q.empty()){
		dot pre=q.top();//取出当前距离上一个点最近的点 
		q.pop();
		if(vis[pre.u]) continue;
		else vis[pre.u]=1;//判断是否走重复路 
		for(int i=head[pre.u];i;i=e[i].nxt){//遍历当前点的所有路径 
			int x=e[i].to;
			if(vis[x]) continue;//不走重复路(贪心策略) 
			if(dis[x]>dis[pre.u]+e[i].w){
				dis[x]=dis[pre.u]+e[i].w;//更新最短路径
				dot nxt;
				nxt.u=x;
				nxt.d=dis[x];
				q.push(nxt);//存入下一个点 
			}
		}
	}
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m>>s;
	build(1,1,n);
	for(int i=1;i<=m;i++){
		cin>>op;
		if(op==1){
			//一号操作直接连边
			cin>>u>>v>>w;
			add(a[u],a[v],w);//直接把对应线段树上的叶子节点连边 
		}else{
			//二三操作通过线段树区间连边
			cin>>u>>l>>r>>w;
			connect(1,l,r,a[u],w,op%2); 
		} 
	}
	for(int i=1;i<=n;i++){
		add(a[i],a[i]+N,0);
		add(a[i]+N,a[i],0);
		//两颗线段树对应位置的叶子节点实际上表示同一个点,连双向边 
	}
	s=a[s]+N;
	/*初始从第二棵线段树的起点对应位置开始跑最短路,因为建的单向边
	保证了只有从第二棵线段树开始往回跑才能走通*/ 
	dij();
	for(int i=1;i<=n;i++){
		if(dis[a[i]]>=inf) cout<<-1<<" ";//无解输出-1
		else cout<<dis[a[i]]<<" ";//输出答案 
	}
	return 0;
}

posted @ 2025-05-07 22:02  Yun_Mo_s5_013  阅读(21)  评论(0)    收藏  举报