【瞎口胡】线段树优化建图

线段树优化建图用来优化一类区间连边的问题。

点向区间连边

image

把区间拆成 \(\log\) 个线段树上的节点,然后连边即可。

线段树上的边含义是「可以从起点到达终点」。而能到达一个区间,自然也可以到达它的两个子区间,因此还需要补上这些边,即图中黑色边(注意黑边从上往下指)。

区间向点连边

类比一下上面。就是把边完全反向。

image

上图是 \([3,7]\) 连向 \(4\) 的情况。

注意此时黑边从下往上指,因为一个小区间没有向某个点连边并不代表这个区间不能到达该点,有可能存在包含它的一个大区间向该点连边。

区间向区间连边

如果只有一棵线段树,似乎不太好处理...考虑一下区间和区间本身的连边,除了连出一个自环之外没有任何用处。

我们换成两棵线段树试一试。其中一棵树为「入树」,作为连边的终点,与第一节中的线段树结构类似;另外一棵树为「出树」,作为连边的起点,与第二节中的线段树结构类似。

image

如图。上半部分是入树,下半部分是出树。要注意,两棵线段树的叶子节点其实是一个点

但是这样,需要连 \(O(\log ^2n)\) 条边,不够优秀。考虑建立虚点,两棵树的点只与虚点相连:

image

为什么是两个虚点,而不是一个?在部分题目中边带权,这个时候就需要给两个虚点相连的那条边赋上权值。

例题 1 Codeforces 786B Legacy

题意

给定 \(n\) 个点和 \(q\) 次操作。每次操作是下面三种类型之一:

  • 给定 \(u,v,w\),连一条权值为 \(w\) 的有向边 \((u,v)\)

  • 给定 \(l,r,v,w\),对于任意 \(u\) 满足 \(l \leq u \leq r\),连一条权值为 \(w\) 的有向边 \((u,v)\)

  • 给定 \(l,r,u,w\),对于任意 \(v\) 满足 \(l \leq v \leq r\),连一条权值为 \(w\) 的有向边 \((u,v)\)

询问 \(s\) 到每个点的最短路,或者指除最短路不存在。

\(1 \leq n,q \leq 10^5, 1 \leq w \leq 10^9\)

题解

两棵线段树。一棵处理点向区间连边,另外一棵处理区间向点连边。当然,仍然需要注意两棵线段树的叶子节点其实是一个点。

# include <bits/stdc++.h>

typedef long long ll;
typedef std::pair <int,int> pii;
const ll INFll=0x3f3f3f3f3f3f3f3fll;
const int N=200010,MAXN=N*4,REF=N*2,INF=0x3f3f3f3f;
struct Node{
	int id;
	ll w;
	bool operator < (const Node &rhs) const{
		return w>rhs.w;
	}
};
std::priority_queue <Node> q;
std::vector <pii> G[MAXN];
int n,m,s;
int id[N];
ll dis[MAXN];
bool c[MAXN];
inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-')f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
inline void add(int u,int v,int w){
	G[u].push_back(std::make_pair(v,w));
	return;
}
inline int lc(int x){
	return x<<1;
}
inline int rc(int x){
	return x<<1|1;
}
void build(int k,int l,int r){
	if(l==r){
		id[l]=k;
		add(k,k+REF,0),add(k+REF,k,0);
		return;
	}
	int mid=(l+r)>>1;
	add(k,lc(k),0),add(k,rc(k),0);
	add(lc(k)+REF,k+REF,0),add(rc(k)+REF,k+REF,0);
	build(lc(k),l,mid),build(rc(k),mid+1,r);
	return;
}
void change(int k,int l,int r,int L,int R,int x,int w,int op){
	if(L<=l&&r<=R){
		(!op)?add(k+REF,id[x],w):add(id[x],k,w);
		return;
	}
	int mid=(l+r)>>1;
	if(L<=mid)
		change(lc(k),l,mid,L,R,x,w,op);
	if(mid<R)
		change(rc(k),mid+1,r,L,R,x,w,op);
	return;
}
inline void Dijkstra(void){
	memset(dis,INF,sizeof(dis));
	dis[s]=0;
	q.push((Node){s,0});
	while(!q.empty()){
		int i=q.top().id;
		q.pop();
		if(c[i])
			continue;
		c[i]=true;
		for(int j=0;j<(int)G[i].size();++j){
			int to=G[i][j].first;
			ll w=G[i][j].second;
			if(dis[to]>dis[i]+w){
				dis[to]=dis[i]+w,q.push((Node){to,dis[to]});
			}
		}
	}
	return;
}
int main(void){
	n=read(),m=read(),s=read();
	build(1,1,n);
	for(int i=1;i<=m;++i){
		int op=read(),v=read(),l,r,w;
		switch(op){
			case 1:{
				l=read(),w=read(),add(id[v],id[l],w);
				break;
			}
			case 2:{
				l=read(),r=read(),w=read(),change(1,1,n,l,r,v,w,1);
				break;
			}
			case 3:{
				l=read(),r=read(),w=read(),change(1,1,n,l,r,v,w,0);
				break;
			}
		}
	}
	s=id[s],Dijkstra();
	for(int i=1;i<=n;++i){
		printf("%lld ",dis[id[i]]==INFll?-1:dis[id[i]]);
	}
	return 0;
}

例题 2 [SNOI2017]炸弹

题意

给定坐标轴上 \(n\) 个炸弹的坐标和引爆半径,第 \(i\) 个炸弹的坐标为 \(x_i\),引爆半径为 \(r_i\)

当一个炸弹被引爆时,它会引爆在爆炸范围内的所有炸弹,即满足 \(|x_i-x_j| \leq r_i\) 的所有炸弹 \(j\)

你需要求出,对于每个炸弹,如果把它引爆,会有多少个炸弹被引爆。

\(1 \leq n \leq 5 \times 10^5, -10^{18} \leq x_i \leq 10^{18},0 \leq r_i \leq 2 \times 10^{18}\)

题解

注意到一个炸弹能引爆的炸弹一定是一段区间(不考虑二次引爆)。二分找到该区间的左右边界,然后线段树优化建图。

似乎可以对该图缩点后进行拓扑排序。把线段树上的叶子节点赋予点权 \(1\),求的就是一个点出发能到达的点权和。然而这不能做:

image

想象中的建反图拓扑排序效果如上图。最左侧的点答案会被错误地统计为 \(2\)

观察到一个炸弹在考虑二次引爆的情况下,能引爆的炸弹也是一段区间。记录下每个点对应的区间,从而可以在缩点时求出每一个强连通分量对应的区间。

之后建反图拓扑排序,维护每个点对应区间的左端点和右端点即可。

# include <bits/stdc++.h>

typedef long long ll;
const int N=500010,MAXN=N*4,INF=0x3f3f3f3f;
const ll MOD=1e9+7;
struct Edge{
	int to,next;
};
struct Graph{
	Edge edge[N*20];
	int head[MAXN],sum;
	inline void add(int x,int y){
		edge[++sum].to=y;
		edge[sum].next=head[x];
		head[x]=sum;
		return;
	}
}A,B;

int n;
ll x[N],R[N];
int id[N];
int nL[MAXN],nR[MAXN];
int maxid;
int col[MAXN],colcnt,t,dfn[MAXN],low[MAXN],cL[MAXN],cR[MAXN],du[MAXN];
bool vis[MAXN];
std::stack <int> K; 

inline ll read(void){
	ll res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-')f=-1;
	res=c-48ll;
	while((c=getchar())>='0'&&c<='9')
		res=res*10ll+c-48;
	return res*f;
}
inline int lc(int x){
	return x<<1;
}
inline int rc(int x){
	return x<<1|1;
}
inline void ckmin(int &x,int val){
	x=std::min(x,val);
}
inline void ckmax(int &x,int val){
	x=std::max(x,val);
	return;
}
void build(int k,int l,int r){
	ckmax(maxid,k);
	nL[k]=l,nR[k]=r;
	if(l==r){
		id[l]=k;
		return;
	}
	int mid=(l+r)>>1;
	A.add(k,lc(k)),A.add(k,rc(k));
	build(lc(k),l,mid),build(rc(k),mid+1,r);
	return;
}
void change(int k,int l,int r,int L,int R,int x){
	if(L<=l&&r<=R){
		if(k!=x)
			A.add(x,k);
		return;
	}
	int mid=(l+r)>>1;
	if(L<=mid)
		change(lc(k),l,mid,L,R,x);
	if(mid<R)
		change(rc(k),mid+1,r,L,R,x);
	return;
}
void tarjan(int i){
	dfn[i]=low[i]=++t,K.push(i),vis[i]=true;
	for(int j=A.head[i];j;j=A.edge[j].next){
		int to=A.edge[j].to;
		if(!vis[to]&&dfn[to])
			continue;
		if(!dfn[to])
			tarjan(to),low[i]=std::min(low[i],low[to]);
		else
			low[i]=std::min(low[i],dfn[to]);
	}
	if(dfn[i]==low[i]){
		++colcnt;
		while(K.top()!=i){
			int x=K.top();
			K.pop(),vis[x]=false,col[x]=colcnt;
			ckmin(cL[colcnt],nL[x]),ckmax(cR[colcnt],nR[x]);
		}
		K.pop(),vis[i]=false,col[i]=colcnt;
		ckmin(cL[colcnt],nL[i]),ckmax(cR[colcnt],nR[i]);
	}
	return;
}
inline void topsort(void){
	std::queue <int> q=std::queue <int> ();
	for(int i=1;i<=n;++i){
		if(!du[i])
			q.push(i);
	}
	while(!q.empty()){
		int i=q.front();
		q.pop();
		for(int j=B.head[i];j;j=B.edge[j].next){
			int to=B.edge[j].to;
			--du[to],ckmin(cL[to],cL[i]),ckmax(cR[to],cR[i]);
			if(!du[to])
				q.push(to); 
		}
	}
	return;
}
int main(void){
	n=read();
	build(1,1,n);
	for(int i=1;i<=n;++i)
		x[i]=read(),R[i]=read();
	for(int i=1;i<=n;++i){
		int l=std::lower_bound(x+1,x+1+n,x[i]-R[i])-x,r=std::upper_bound(x+1,x+1+n,x[i]+R[i])-x-1;
		if(l<=r)
			change(1,1,n,l,r,id[i]);
	}
	memset(cL,INF,sizeof(cL)),memset(cR,-INF,sizeof(cR));
	for(int i=1;i<=maxid;++i)
		if(!dfn[i])
			tarjan(i);
	for(int i=1;i<=maxid;++i){
		for(int j=A.head[i];j;j=A.edge[j].next){
			int to=A.edge[j].to;
			if(col[i]!=col[to]){
				B.add(col[to],col[i]),++du[col[i]];
			}
		}
	}
	topsort();
	ll ans=0;
	for(int i=1;i<=n;++i){
		ans=(ans+1ll*i*(cR[col[id[i]]]-cL[col[id[i]]]+1))%MOD;
	}
	printf("%lld",ans);
	return 0;
}

更多

[PA2011]Journeys

原文地址:https://www.cnblogs.com/liuzongxin/p/15256565.html