线段树优化建图

线段树优化建图

trik

线段树优化建图,顾名思义利用线段树建图优化时间复杂度,常用于以下操作

  • 区间向区间连边
  • 区间向点连边
  • 点向区间连边

若使用暴力连边,时间复杂度是O(n2)O(n^2)O(n2),但是如果我们基于线段树区间分解的思想,将一个区间分解为最多lognlognlogn个区间再去连边,时间复杂度就可以降为O(nlogn)O(nlogn)O(nlogn)

例如对于8个点,我们现在要将点2向区间[3,7][3,7][3,7]连边,那么我们可以将2分别连向[3,4],[5,6],[7,7][3,4],[5,6],[7,7][3,4],[5,6],[7,7],具体到线段树上就是向这三个区间对应的编号连边。因为每个区间都可以访问到其子区间的点,所以我们应该将所有区间对其子区间建代价为0的边。

对于区间向点连边,我们反过来建边,即所要操作的所有区间向点连边,同样我们要把子区间向父区间连边

但是若两种操作同时存在时,只开一颗线段树明显会出现错误,因为每个区间都可以0代价的访问到其他区间,由此我们引入出树和入树的概念

  • 入树:
    只连自下向上的边,即子向父连边
    负责将实际起点或起点区间聚合到高层状态
  • 出树:
    只连自上向下的边,即父向子连边
    将终点区间分发到具体的点

同时我们还要将出树与入数的对应叶子节点连代价为0的边,代表其能相互转换

所以对于上面的操作我们可以这样实现

  1. 点向区间连边:将出树的对应节点向入树的对应区间节点连边
  2. 区间向点连边:将入树的对应区间节点向出树的对应节点连边
  3. 区间向区间连边:建立一个虚点,分别向两个区间连边,一条边权为w,一条边权为0,由于是双向边,需要建立
  • 出树 →\rightarrow→ 虚点 →\rightarrow→ 入树
  • 入树 →\rightarrow→ 虚点 →\rightarrow→ 出树

例题

cf786B

题意

nnn个点mmm次操作,每次操作为下面三种之一,求给定点到其他点的最短路

  1. 点向点连边
  2. 点向区间连边
  3. 区间向点连边
    1≤n,m≤1051 \leq n,m \leq 10^51≤n,m≤105
思路

毫无疑问的板子题,开两棵线段树对应连边即可,对于点向点连边我们将出树和入树的叶子节点连边即可,这里我们选择用偏移量代表入树,需要注意开ll,一般线段树的大小为4∗N4*N4∗N,开两颗我们需要8∗N8*N8∗N的大小,同时偏移量取4∗N4*N4∗N即可,需要注意一定是入树向出树连边,需要额外开一个数组记录叶子节点在线段树上所对应的编号

代码
c 复制代码
#include<bits/stdc++.h>

#define ull unsigned long long 
#define ll long long
#define inf 1e9
#define INF 1e18
#define lc p<<1
#define rc p<<1|1
#define endl '\n'
#define all(a) a.begin()+1,a.end()
#define all0(a) a.begin(),a.end()
#define lowbit(a) (a&-a)
#define fi first
#define se second
#define pb push_back
#define yes cout<<"YES"<<endl
#define no cout<<"NO"<<endl

using namespace std;
const double eps=1e-6;
typedef pair<ll,ll>PII;
typedef array<int,3>PIII;
mt19937_64 rnd(time(0));  

const int N=1e6+5;
const int K=5e5;//入树偏移

ll tr[N];
ll leaf[N];//叶子节点编号
struct edge{
	ll v,w;
};
vector<edge>e[N];

ll dis[N];
ll vis[N];


void build(int p,int l,int r)
{
	if(l==r) 
	{
		leaf[l]=p;
		return;
	}
	int mid=(l+r)>>1;
	e[p].pb({lc,0});
	e[p].pb({rc,0});
	e[(lc)+K].pb({p+K,0});
	e[(rc)+K].pb({p+K,0});
	build(lc,l,mid);
	build(rc,mid+1,r);
}

void update(int p,int l,int r,int v,int ul,int ur,int w,int op)
{
	if(ul<=l && ur>=r)
	{
		if(op==2) e[leaf[v]+K].pb({p,w});
		else e[p+K].pb({leaf[v],w});
		return;
	}
	int mid=(l+r)>>1;
	if(ul<=mid) update(lc,l,mid,v,ul,ur,w,op);
	if(ur>mid) update(rc,mid+1,r,v,ul,ur,w,op);
}


void solve()
{
	ll n,m,s;
	cin>>n>>m>>s;
	build(1,1,n);
	for(int i=1;i<=n;i++)
	{
		e[leaf[i]].pb({leaf[i]+K,0});
		e[leaf[i]+K].pb({leaf[i],0});
	}
	
	while(m--)
	{
		int op;cin>>op;
		if(op==1)
		{
			ll u,v,w;cin>>u>>v>>w;
			e[leaf[u]+K].pb({leaf[v],w});
		}
		else
		{
			ll v,l,r,w;
			cin>>v>>l>>r>>w;
			update(1,1,n,v,l,r,w,op);
		}
	}
	
	memset(dis,0x3f,sizeof dis);
	priority_queue<PII,vector<PII>,greater<PII>>q;
	q.push({0,leaf[s]});
	
	
	while(!q.empty())
	{
		auto [cost,cur]=q.top();
		q.pop();
		
		if(vis[cur]) continue;
		vis[cur]=1;
		
		for(auto [v,w]:e[cur])
		{
			if(cost+w<dis[v])
			{
				dis[v]=cost+w;
				q.push({dis[v],v});
			}
		}
	}
	
	for(int i=1;i<=n;i++)
	{
		if(dis[leaf[i]]==0x3f3f3f3f3f3f3f3fll) cout<<-1<<" ";
		else cout<<dis[leaf[i]]<<" ";
	}
	cout<<endl;
}
	
	
int main()
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	
	solve();
	
	return 0;
}

P5025炸弹

题意

在一条直线上有 nnn 个炸弹,每个炸弹的坐标是 x_i ,爆炸半径是 r_i ,当一个炸弹爆炸时,如果另一个炸弹所在位置 x_j 满足:

\|x_j-x_i\| \\le r_i ,那么,该炸弹也会被引爆。

现在,请你帮忙计算一下,先把第 iii 个炸弹引爆,将引爆多少个炸弹呢?

答案对 109+710^9 + 7109+7 取模

第一行,一个数字 nnn ,表示炸弹个数。

第 2∼n+12 \sim n+12∼n+1 行,每行两个整数,表示 xix_ixi,rir_iri,保证 xix_ixi 严格递增。

一个数字,表示 ∑i=1ni×\sum \limits_{i=1}^n i\timesi=1∑ni× 炸弹 iii 能引爆的炸弹个数。

对于 20%20\%20% 的数据: n≤100n\leq 100n≤100。

对于 50%50\%50% 的数据: n≤1000n\leq 1000n≤1000。

对于 80%80\%80% 的数据: n≤100000n\leq 100000n≤100000。

对于 100%100\%100% 的数据: 1≤n≤5000001\le n\leq 5000001≤n≤500000,−1018≤xi≤1018-10^{18}\leq x_{i}\leq 10^{18}−1018≤xi≤1018,0≤ri≤2×10180\leq r_{i}\leq 2\times 10^{18}0≤ri≤2×1018。

思路

很容易能想到可以将每个炸弹向在其引爆范围的其他炸弹建边,每个点所能到达点的个数就是答案,但这样做是O(n2)O(n^2)O(n2)的,无疑不能接受,那么我们就可以用线段树优化建图,建完图之后为了方便统计答案,我们可以用tarjan缩点,缩点过程中我们记录每个连通块内的点所能影响到的最左侧和最右侧点,这样对于一个点,其贡献就是其编号乘以其所在连通块中最右侧的点减去最左侧的点(直接使用连通块大小会重复计算),但是这样还不够,缩点完之后还会有外部的点可以访问到连通块的情况,所以我们缩点之后应该再跑一次dfs更新每个点所能访问到的最左侧点和最右侧点,最后计算答案即可

代码
c 复制代码
#include<bits/stdc++.h>
#define ull unsigned long long 
#define ll long long
#define inf 1e9
#define INF 1e18
#define lc p<<1
#define rc p<<1|1
#define endl '\n'
#define all(a) a.begin()+1,a.end()
#define all0(a) a.begin(),a.end()
#define lowbit(a) (a&-a)
#define fi first
#define se second
#define pb push_back
#define yes cout<<"YES"<<endl
#define no cout<<"NO"<<endl

using namespace std;
const double eps=1e-6;
typedef pair<int,int>PII;
typedef array<int,3>PIII;
mt19937_64 rnd(time(0));  

const int N=5e5+10;
const int mod=1e9+7;

struct node{
	ll l,r;
}tr[N*4];

ll Left[N*4];
ll Right[N*4];//每个连通块能影响到的最远两端点
vector<ll>e[N*4];
vector<ll>e1[N*4];
ll leaf[N];
ll id;


ll dfn[N*4],low[N*4],tot;
ll stk[N*4],instk[N*4],top;
ll scc[N*4],siz[N*4],cnt;
ll vis[N*4];

void build(ll p,ll l,ll r)
{
	id=max(id,p);
	tr[p]={l,r};
	if(l==r) {leaf[l]=p;return;}
	ll mid=(l+r)>>1;
	e[p].pb(lc);
	e[p].pb(rc);
	build(lc,l,mid);
	build(rc,mid+1,r);
}

void update(ll p,ll l,ll r,ll v,ll ul,ll ur)
{
	if(ul<=l && ur>=r)
	{
		e[v].pb(p);
		return;
	}
	ll mid=(l+r)>>1;
	if(ul<=mid) update(lc,l,mid,v,ul,ur);
	if(ur>mid) update(rc,mid+1,r,v,ul,ur);
}


void tarjan(ll x)
{
	dfn[x]=low[x]=++tot;
	stk[++top]=x;
	instk[x]=1;
	
	for(auto ed:e[x])
	{
		if(!dfn[ed])
		{
			tarjan(ed);
			low[x]=min(low[x],low[ed]);
		}
		else if(instk[ed]) low[x]=min(low[x],dfn[ed]);
	}
	if(dfn[x]==low[x])
	{
		ll y;
		cnt++;
		do
		{
			y=stk[top--];
			instk[y]=0;
			scc[y]=cnt;
			++siz[cnt];
			Left[cnt]=min(Left[cnt],tr[y].l);
			Right[cnt]=max(Right[cnt],tr[y].r);
			//cout<<cnt<<" "<<Left[cnt]<<" "<<Right[cnt]<<endl;
		}while(y!=x);
	}
}

void dfs(ll u)
{
	vis[u]=1;
	for(auto ed:e1[u])
	{
		if(vis[ed])
		{
			Left[u]=min(Left[u],Left[ed]);
			Right[u]=max(Right[u],Right[ed]);
			continue;
		}
		dfs(ed);
		Left[u]=min(Left[u],Left[ed]);
		Right[u]=max(Right[u],Right[ed]);
	}
}

void solve()
{
	ll n;cin>>n;
	vector<ll>x(n+1),r(n+1);
	for(int i=1;i<=n;i++) cin>>x[i]>>r[i];
	build(1,1,n);
	
	memset(Left,0x3f,sizeof Left);
	
	for(int i=1;i<=n;i++)
	{
		if(!r[i]) continue;
		ll L=lower_bound(all(x),x[i]-r[i])-x.begin();
		ll R=upper_bound(all(x),x[i]+r[i])-x.begin();
		R--;
		update(1,1,n,leaf[i],L,R);
		tr[leaf[i]]={L,R};
		
	}
	
	
	for(int i=1;i<=id;i++) if(!dfn[i]) tarjan(i);
	
	for(int i=1;i<=id;i++)
	{
		for(auto ed:e[i])
		{
			if(scc[ed]!=scc[i])
			{
				e1[scc[i]].pb(scc[ed]);
			}
		}
	}
	
	//for(int i=1;i<=cnt;i++) cout<<Left[i]<<" "<<Right[i]<<endl;
	for(int i=1;i<=id;i++) if(!vis[i]) dfs(i);
	
	
//	for(int i=1;i<=cnt;i++) cout<<Left[i]<<" "<<Right[i]<<endl;
	
	ll ans=0;
	for(int i=1;i<=n;i++)
	{
		ans=(ans+i*(Right[scc[leaf[i]]]-Left[scc[leaf[i]]]+1)%mod)%mod;
	}
	cout<<ans;
}
	
	
int main()
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	
	solve();
	
	return 0;
}

P6348 Journeys

题意

一个星球上有 nnn 个国家和许多双向道路,国家用 1∼n1\sim n1∼n 编号。

但是道路实在太多了,不能用通常的方法表示。于是我们以如下方式表示道路:(a,b),(c,d)(a,b),(c,d)(a,b),(c,d) 表示,对于任意两个国家 x,yx,yx,y,如果 a≤x≤b,c≤y≤da\le x\le b,c\le y\le da≤x≤b,c≤y≤d,那么在 x,yx,yx,y 之间有一条道路。

首都位于 PPP 号国家。你想知道 PPP 号国家到任意一个国家最少需要经过几条道路。保证 PPP 号国家能到任意一个国家。

第一行三个整数 n,m,Pn,m,Pn,m,P。

之后 mmm 行,每行 444 个整数 a,b,c,da,b,c,da,b,c,d。

nnn 行,第 iii 行表示 PPP 号国家到第 iii 个国家最少需要经过几条路。

对于所有测试点,保证 1≤n≤5×1051\le n\le 5\times 10^51≤n≤5×105,1≤m≤1051\le m\le 10^51≤m≤105,1≤a≤b≤n1\le a\le b\le n1≤a≤b≤n,1≤c≤d≤n1\le c\le d\le n1≤c≤d≤n。

思路

区间向区间连边,如果直接两个区间连的话是O((logn)2)O((logn)^2)O((logn)2),可以对于每个区间开一个虚点,这样将两个区间分别连向这个虚点即可降到O(logn)O(logn)O(logn),由于是双向边所以需要建立两次,即

  • 出树 →\rightarrow→ 虚点 →\rightarrow→ 入树
  • 入树 →\rightarrow→ 虚点 →\rightarrow→ 出树
    由于出树和入树叶子节点互相连边,所以哪个当起点和终点都无所谓
代码
c 复制代码
#include<bits/stdc++.h>

#define ull unsigned long long 
#define ll long long
#define inf 1e9
#define INF 1e18
#define lc p<<1
#define rc p<<1|1
#define endl '\n'
#define all(a) a.begin()+1,a.end()
#define all0(a) a.begin(),a.end()
#define lowbit(a) (a&-a)
#define fi first
#define se second
#define pb push_back
#define yes cout<<"YES"<<endl
#define no cout<<"NO"<<endl

using namespace std;
const double eps=1e-6;
typedef pair<int,int>PII;
typedef array<int,3>PIII;
mt19937_64 rnd(time(0));  

const int N=5e5+10;

int leaf[N];

struct edge{
	int v,w;
};
vector<edge>e[N<<4];
int n,m,p;
int tot;//虚点偏移量
int dis[N<<4];
int vis[N<<4];
void build(int p,int l,int r)
{
	if(l==r)
	{
		leaf[l]=p;
		return;
	}
	e[p].pb({lc,0});
	e[p].pb({rc,0});
	e[(lc)+n*4].pb({p+n*4,0});
	e[(rc)+n*4].pb({p+n*4,0});
	int mid=(l+r)>>1;
	build(lc,l,mid);
	build(rc,mid+1,r);
}
//0:入树向虚点连,1:虚点向出树连
void update(int p,int l,int r,int ul,int ur,int op)
{
	if(ul<=l && ur>=r)
	{
		if(!op) e[p+n*4].pb({n*8+tot,1});
		else e[n*8+tot].pb({p,0});
		return;
	}
	int mid=(l+r)>>1;
	if(ul<=mid) update(lc,l,mid,ul,ur,op);
	if(ur>mid) update(rc,mid+1,r,ul,ur,op);
}


void solve()
{
	
	cin>>n>>m>>p;
	build(1,1,n);
	for(int i=1;i<=n;i++)
	{
		e[leaf[i]+4*n].pb({leaf[i],0});
		e[leaf[i]].pb({leaf[i]+4*n,0});
	}
	
	for(int i=1;i<=m;i++)
	{
		int a,b,c,d;
		cin>>a>>b>>c>>d;
		tot++;
		update(1,1,n,a,b,0);//出树
		update(1,1,n,c,d,1);
		
		tot++;
		update(1,1,n,a,b,1);
		update(1,1,n,c,d,0);
	}
	
	priority_queue<PII,vector<PII>,greater<PII>>q;
	q.push({0,leaf[p]});
	memset(dis,0x3f,sizeof dis);
	dis[leaf[p]]=0;
	
	
	while(!q.empty())
	{
		auto [cost,cur]=q.top();
		q.pop();
		
		if(vis[cur]) continue;
		vis[cur]=1;
		
		for(auto [v,w]:e[cur])
		{
			if(cost+w<dis[v])
			{
				dis[v]=cost+w;
				q.push({dis[v],v});
			}
		}
	}
	
	for(int i=1;i<=n;i++) cout<<dis[leaf[i]]<<endl;
	
}
	
	
int main()
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	
	solve();
	
	return 0;
}

atcoder NIKKEI Programming Contest 2019-2 D - Shortest Path on a Line

题意

给定nnn个点,mmm行形如a,b,ca,b,ca,b,c的建边方式,代表[a,b][a,b][a,b]区间内的点都互相建立一条边权为ccc的点,求1到n的最短路径

思路

即区间向区间连边,只不过是这个区间向自己连边,那么我们在加边时只加一次即可

代码
c 复制代码
#include<bits/stdc++.h>

#define ull unsigned long long 
#define ll long long
#define inf 1e9
#define INF 1e18
#define lc p<<1
#define rc p<<1|1
#define endl '\n'
#define all(a) a.begin()+1,a.end()
#define all0(a) a.begin(),a.end()
#define lowbit(a) (a&-a)
#define fi first
#define se second
#define pb push_back
#define yes cout<<"YES"<<endl
#define no cout<<"NO"<<endl

using namespace std;
const double eps=1e-6;
typedef pair<ll,int>PII;
typedef array<int,3>PIII;
mt19937_64 rnd(time(0));  

const int N=2e5+10;
int leaf[N];
struct edge{
	ll v,w;
};
ll dis[N<<4];
int vis[N<<4];
vector<edge>e[N<<4];
int n,m;
int tot;

void build(int p,int l,int r)
{
	if(l==r)
	{
		leaf[l]=p;
		return;
	}
	e[p].pb({lc,0});
	e[p].pb({rc,0});
	e[(lc)+4*n].pb({p+4*n,0});
	e[(rc)+4*n].pb({p+4*n,0});
	int mid=(l+r)>>1;
	build(lc,l,mid);
	build(rc,mid+1,r);
}

void update(int p,int l,int r,int ul,int ur,int w,int op)
{
	if(ul<=l && ur>=r)
	{
		if(!op) e[p+4*n].pb({8*n+tot,w});
		else e[8*n+tot].pb({p,0});
		return;
	}
	int mid=(l+r)>>1;
	if(ul<=mid) update(lc,l,mid,ul,ur,w,op);
	if(ur>mid) update(rc,mid+1,r,ul,ur,w,op);
}

void solve()
{
	cin>>n>>m;
	build(1,1,n);
	for(int i=1;i<=n;i++)
	{
		e[leaf[i]].pb({leaf[i]+4*n,0});
		e[leaf[i]+4*n].pb({leaf[i],0});
	}
	
	for(int i=1;i<=m;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		
		tot++;
		update(1,1,n,a,b,c,0);
		update(1,1,n,a,b,c,1);
		
		// tot++;
		// update(1,1,n,a,b,c,1);
		// update(1,1,n,a,b,c,0);
	}
	
	priority_queue<PII,vector<PII>,greater<PII>>q;
	q.push({0,leaf[1]+4*n});
	memset(dis,0x3f,sizeof dis);
	dis[leaf[1]+4*n]=0;
	
	while(!q.empty())
	{
		auto [cost,cur]=q.top();
		q.pop();
		
		if(vis[cur]) continue;
		vis[cur]=1;
		
		for(auto [v,w]:e[cur])
		{
			if(cost+w<dis[v])
			{
				dis[v]=cost+w;
				q.push({dis[v],v});
			}
		}
	}
	cout<<(dis[leaf[n]]==0x3f3f3f3f3f3f3f3f?-1:dis[leaf[n]])<<endl;
}
	
	
int main()
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	
	solve();
	
	return 0;
}
相关推荐
WaWaJie_Ngen2 小时前
C++实现一笔画游戏
c++·算法·游戏·游戏程序·课程设计
程序员-King.2 小时前
day140—前后指针—删除排序链表中的重复元素Ⅱ(LeetCode-82)
数据结构·算法·leetcode·链表
小尧嵌入式2 小时前
【Linux开发一】类间相互使用|继承类和构造写法|虚函数实现多态|五子棋游戏|整数相除混合小数|括号使用|最长问题
开发语言·c++·算法·游戏
Remember_9932 小时前
【JavaSE】一站式掌握Java面向对象编程:从类与对象到继承、多态、抽象与接口
java·开发语言·数据结构·ide·git·leetcode·eclipse
皮蛋sol周2 小时前
嵌入式学习数据结构(二)双向链表 内核链表
linux·数据结构·学习·嵌入式·arm·双向链表
你的冰西瓜2 小时前
C++中的map容器详解
开发语言·c++·stl
BHXDML2 小时前
第三章:聚类算法
算法·机器学习·聚类
仙俊红2 小时前
二分查找边界模板:第一个 > target / 第一个 < target(找不到就返回边界)
算法
苦藤新鸡2 小时前
16.求数组除了当前元素的所有乘积
算法·leetcode·动态规划