线段树优化建图详解——区间连边之技巧,吊打紫题之利器

我们从一道例题开始。

CF786B

Description

在这里插入图片描述

Solution

朴素解法: 暴力连边+最短路

对于每次连边操作,我们逐一连边,最后在图上跑一遍单源最短路径算法即可。

时间复杂度 O ( n 2 log ⁡ ( n 2 ) ) O(n^2 \log (n^2)) O(n2log(n2))

正解: 线段树优化建图

线段树有一个非常优美的性质: 区间 [ l , r ] [l,r] [l,r]可以被映射成线段树上的许多连续的区间,且这些区间的数量不超过 ⌈ log ⁡ n ⌉ \lceil \log n \rceil logn

我们要巧妙运用这个性质——我们是否可以将每一个连边的区间 [ l , r ] [l,r] [l,r]映射到线段树上的 log ⁡ \log log个节点,然后只向这 log ⁡ \log log个节点连边呢?

答案是可以的。我们建立两棵线段树,一个线段树往内连边(简称为入树),另一个线段树往外连边(简称为出树)。每棵树的叶节点对应图中一个的真实节点。同时,两棵树中对应的叶节点连一条边权为 0 0 0的有向边(即下图中五彩缤纷的那些边)。同一棵树中的父节点与孩子节点也要连一条边权为 0 0 0的有向边。

在这里插入图片描述

对于每次连边操作,我们只向 log ⁡ \log log个节点连一条有向边,边权为 w w w
在这里插入图片描述
上图表示一个形如“从 1 1 1号节点向区间 [ 3 , 8 ] [3,8] [3,8]中的点分别连一条边”的第二类操作。第三类操作同理。第一类操作直接将对应的叶节点连边。

最后我们跑一遍单源最短路(Dijkstra)即可。

注意,这里的最短路的“源”是出树中表示区间 [ 1 , 1 ] [1,1] [1,1]的叶节点。

由于边数为 O ( n log ⁡ n ) O(n \log n) O(nlogn)级别,所以总时间复杂度为 O ( n log ⁡ n log ⁡ ( n log ⁡ n ) ) ≈ O ( n   l o g 2 n ) O(n \log n \log (n \log n))≈O(n\ log^2 n) O(nlognlog(nlogn))O(n log2n)

Code

#include <bits/stdc++.h>
#define int long long
#define inf 200000000000007
using namespace std;
const int maxl=100005,maxg=20;

int read(){
	int s=0,w=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-')  w=-w;ch=getchar();}
	while (ch>='0'&&ch<='9'){s=(s<<1)+(s<<3)+(ch^'0');ch=getchar();}
	return s*w;
}
int n,m,s,blo,opt,u,l,r,w,cnt=0;
int head[8*maxl],itree[4*maxl],otree[4*maxl],pos[maxl];
int dis[8*maxl],vis[8*maxl];

struct edge{int nxt,to,dis;}e[4*maxg*maxl];//边数可能较多
struct node{
	int dis,pos;
	bool operator < (const node &x) const{return x.dis<dis;}
};

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

void build_itree(int l,int r,int rt){
	if (l==r){
		pos[l]=rt;
		add_edge(rt+blo,rt,0),add_edge(rt,rt+blo,0);
		return;
	}
	int mid=(l+r)>>1;
	add_edge(rt,2*rt,0),build_itree(l,mid,2*rt);
	add_edge(rt,2*rt+1,0),build_itree(mid+1,r,2*rt+1);
}

void build_otree(int l,int r,int rt){
	if (rt!=1)  add_edge(blo+rt,blo+(rt/2),0); 
	if (l==r)  return;
	
	int mid=(l+r)>>1;
	build_otree(l,mid,2*rt);
	build_otree(mid+1,r,2*rt+1);
}

void tree_edge(int nl,int nr,int l,int r,int rt,int link_pos,int ww,int k){
	if (nl<=l&&r<=nr){
		if (k==0)  add_edge(link_pos+blo,rt,ww);
		else add_edge(rt+blo,link_pos,ww);
		return;
	}
	int mid=(l+r)>>1;
	if (nl<=mid)  tree_edge(nl,nr,l,mid,2*rt,link_pos,ww,k);
	if (nr>mid)  tree_edge(nl,nr,mid+1,r,2*rt+1,link_pos,ww,k);
}

std::priority_queue<node> q;
void dijkstra(){
	q.push((node){0,s});
	dis[s]=0;
	while (!q.empty()){
		int now=q.top().pos;
		q.pop();
		if (vis[now])  continue;
		vis[now]=1;
		
		for (int i=head[now];i;i=e[i].nxt){
			int y=e[i].to;
			if (dis[y]>dis[now]+e[i].dis){
				dis[y]=dis[now]+e[i].dis;
				if (!vis[y])  q.push((node){dis[y],y});
			}
		}
	}
}

signed main(){
	n=read(),m=read(),s=read();blo=4*n;
	build_itree(1,n,1);
	build_otree(1,n,1);//给树中的每个节点一个统一的编号
	for (int i=1;i<=m;i++){
		opt=read(),u=read();
		if (opt==1){
			l=read(),w=read();
			tree_edge(l,l,1,n,1,pos[u],w,0);
		}
		else if (opt==2){
			l=read(),r=read(),w=read();
			tree_edge(l,r,1,n,1,pos[u],w,0);
		}
		else if (opt==3){
			l=read(),r=read(),w=read();
			tree_edge(l,r,1,n,1,pos[u],w,1);
		}
	}
	s=blo+pos[s];
	for (int i=1;i<=8*n;i++)  dis[i]=inf;
	
	dijkstra();
	for (int i=1;i<=n;i++){
		if (dis[pos[i]]>=inf)  dis[pos[i]]=-1;
	}
	for (int i=1;i<=n;i++)  printf("%lld ",dis[pos[i]]);
	return 0;
}

一个小扩展: 第四类操作

第四类操作是区间向区间连边,即 [ l 1 , r 1 ] [l_1,r_1] [l1,r1]中的每个节点向 [ l 2 , r 2 ] [l_2,r_2] [l2,r2]中的每个节点连一条边权为 w w w的边。

对于这一种新的操作我们该怎么办呢?不难想到,我们可以新建一个虚点 p p p,然后就变成了“ [ l 1 , r 1 ] [l_1,r_1] [l1,r1] p p p连边”与“ p p p [ l 2 , r 2 ] [l_2,r_2] [l2,r2]连边”,分别处理即可。

显然第四类操作并没有对时间复杂度有太大的影响(就是常数变大了好多……),依然是 O ( n log ⁡ 2 n ) O(n \log^2 n) O(nlog2n)

例题: P5025

Description

在这里插入图片描述

Solution

算法一: 套路,朴素,时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( n 2 32 ) O(\frac {n^2} {32}) O(32n2)

对于两个能够互相引爆的节点我们连一条边。

显然,所有第 i i i个炸弹能够引爆的炸弹就是所有从 i i i出发能够到达的炸弹。这是一个可达性统计问题。我们采用 b i t s e t bitset bitset去转移即可。

算法二: 连边的性质与线段树优化建图

不难发现,第 i i i个节点连向的所有节点一定在一个连续的区间 [ L , R ] [L,R] [L,R]内。

于是,我们可以对于每一个 i i i二分出 L L L R R R然后线段树优化建图即可。最后再跑一遍可达性统计。

时间复杂度与空间复杂度不变。

算法三: 小性质得到正解

考虑第 i i i个节点可达的节点映射下来一定是一个区间。

所以我们并不需要 b i t s e t bitset bitset这种大空间&大时间复杂度写法,我们只需要求出从第 i i i个节点能够到达的区间的左右端点即可,区间长度即为可达的炸弹数量。这可以通过缩点+ D A G DAG DAG D P DP DP求出。

时间复杂度被我们优化成了 O ( n log ⁡ n ) O(n \log n) O(nlogn)

Code

#include <bits/stdc++.h>
#define ll long long
#define inf 2000000007
using namespace std;
const int maxl=500005,maxt=1500005,maxg=19,mod=1e9+7;

ll read(){
	ll s=0,w=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-')  w=-w;ch=getchar();}
	while (ch>='0'&&ch<='9'){s=(s<<1ll)+(s<<3ll)+(ch^'0');ch=getchar();}
	return s*w;
}
int n,tot,blo,len,cnt,nt;ll ans=0;
int head[maxt],tree[maxl*2][2],dfn[maxt],low[maxt],s[maxt];
int fa[maxt],lm[maxt],rm[maxt],inde[maxt];
ll a[maxl],b[maxl];

bitset<maxt> vis,is_fa;
queue<int> q;
map<pair<int,int>,bool> ma;

struct node{int x,y;}edge_lis[2*maxl+maxl*maxg];
struct edge{int nxt,to;}e[2*maxl+maxl*maxg];

void add_edge(int u,int v){
	cnt++;
	e[cnt].to=v,e[cnt].nxt=head[u],head[u]=cnt;
}

void clear_edges(){
	cnt=0;
	for (int i=1;i<=3*n;i++)  head[i]=0;
}

int build_tree(int l,int r,int rt){
	rt=++tot;
	if (l==r){
		add_edge(rt,2*n+l);
		add_edge(2*n+l,rt);
		return rt;
	}
	int mid=(l+r)>>1;
	tree[rt][0]=build_tree(l,mid,0);
	tree[rt][1]=build_tree(mid+1,r,0);
	add_edge(rt,tree[rt][0]);
	add_edge(rt,tree[rt][1]);
	
	return rt;
}

void tree_edge(int nl,int nr,int l,int r,int rt,int k){
	if (nl<=l&&r<=nr){
		add_edge(k,rt);
		return;
	}
	int mid=(l+r)>>1;
	if (nl<=mid)  tree_edge(nl,nr,l,mid,tree[rt][0],k);
	if (nr>mid)  tree_edge(nl,nr,mid+1,r,tree[rt][1],k);
}

void tarjan(int now){
	dfn[now]=low[now]=++nt;
	s[++len]=now,vis[now]=1;
	for (int i=head[now];i;i=e[i].nxt){
		int y=e[i].to;
		if (!dfn[y]){
			tarjan(y);
			low[now]=min(low[now],low[y]);
		}
		else if (vis[y])  low[now]=min(low[now],dfn[y]);
	}
	if (dfn[now]==low[now]){
		int y;
		while (y=s[len]){
			fa[y]=now,vis[y]=0;
			len--;
			if (y>2*n){
				lm[now]=min(lm[now],y-2*n);
				rm[now]=max(rm[now],y-2*n);
			}
			if (y==now)  break;
		}
	}
}

signed main(){
	n=read();
	build_tree(1,n,0);
	for (int i=1;i<=n;i++)  a[i]=read(),b[i]=read();
	for (int i=1;i<=n;i++){
		int p=lower_bound(a+1,a+n+1,a[i]-b[i])-a;
		int p2=upper_bound(a+1,a+n+1,a[i]+b[i])-a-1;
		if (p<p2&&p>=1&&p2<=n)  tree_edge(p,p2,1,n,1,i+2*n);
	}
	for (int i=1;i<=3*n;i++)  lm[i]=inf,rm[i]=0;
	tarjan(1);
	nt=0;
	for (int i=1;i<=3*n;i++)  is_fa[fa[i]]=1;
	for (int now=1;now<=3*n;now++){
		for (int i=head[now];i;i=e[i].nxt){
			int y=e[i].to;
			if (fa[now]!=fa[y]){
				edge_lis[++nt].x=fa[y];
				edge_lis[nt].y=fa[now];
			}
		}
	}
	clear_edges();
	for (int i=1;i<=nt;i++){
		int fx=edge_lis[i].x,fy=edge_lis[i].y;
		if (!ma[make_pair(fx,fy)]){
			add_edge(fx,fy);inde[fy]++;
			ma[make_pair(fx,fy)]=ma[make_pair(fy,fx)]=1;
		}
	}
	for (int i=1;i<=3*n;i++){
		if (is_fa[i]&&(!inde[i]))  q.push(i);
	}
	while (!q.empty()){
		int now=q.front();
		q.pop();
		
		for (int i=head[now];i;i=e[i].nxt){
			int y=e[i].to;
			inde[y]--;
			lm[y]=min(lm[y],lm[now]);
			rm[y]=max(rm[y],rm[now]);
			if (!inde[y])  q.push(y);
		}
	}
	for (int i=1;i<=n;i++)
	  ans=(ans+1ll*i*(rm[fa[i+2*n]]-lm[fa[i+2*n]]+1ll)%mod)%mod;
	cout<<ans<<endl;
	
	return 0;
}
  • 11
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值