zoukankan      html  css  js  c++  java
  • 【瞎口胡】线段树优化建图

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

    点向区间连边

    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

  • 相关阅读:
    SET TEXTSIZE number
    Oracle 参数之_small_table_threshold
    Oracle等待事件db file parallel read
    ORA-12631 / TNS-12631: Username retrieval failed
    Oracle的Connect By理解
    ORA-01436: 用户数据中的CONNECT BY 循环
    Cortex-M3启动深度解析
    【SmartOS】轻量级多任务调度系统
    物联网智能硬件设备身份验证机制
    物联网智能硬件设备常见攻击方法
  • 原文地址:https://www.cnblogs.com/liuzongxin/p/15256565.html
Copyright © 2011-2022 走看看