\(\\\)
适用情况
当点数很多,并且边集以区间形式 \((\) 点 \(\to\) 区间 \(/\) 区间 \(\to\) 点 \()\) 给出时,我们发现强行连边复杂度太大,考虑如何维护同一个子区间的连边,容易想到线段树。
\(\\\)
优化思路
我们先考虑 点 \(\to\) 区间 连边的部分。
考虑建图的复杂度真正体现在哪里。
如果所有的被连边区间不交,那么总复杂度是对的。
所以真正建图的复杂度,产生在被连边区间相交的部分,尤其是一个同一个区间被多个点连边,那么我们每一个都要产生区间长度的代价。
那么有一个思路,如果有一个区间被多次连边,那么不妨我们建立这个区间的一个代表节点。
首先预处理的时候将这个代表节点连向区间里的每一个节点,边权为 \(0\) ,就代表了连向代表节点的每一条边都连向了区间里的每一个节点,然后都每次都将边连向这个代表节点即可。
\(\\\)
建图过程 (点 \(\to\) 区间)
但是注意到这种区间并不是很好找,这种找区间的方法并不是具有很强的普适性。
我们知道线段树的每一个节点维护的是一个固定的区间,线段树的区间操作是 \(log\) 的。
回忆区间加的过程,如果一个节点的代表区间被完全覆盖,那么我们会在这个节点上打标记,等到需要的时候再下传。这体现了一种代表的关系,即区间共有的性质我可以只标记在代表这个区间的节点上。
于是有了一个思路,如果要从一个点连向一个区间,那么可以像区间加那样,把这个点向完全覆盖的区间连边。
也就是说,在区间连边时,在线段树上每找到一个完全覆盖的节点,就将出发点向这个区间的代表节点连边。
同样的,我们先考虑预处理。
一个节点被连边,代表这个节点的 子树 都可以到达。
之所以强调子树,而不是强调区间里的每一个节点,因为我们预处理的边只会向下 连一层 。
只连下一层的边数量级是 \(O(2N)\) ,而连向区间里的每一个节点显然每一层都是 \(O(N)\) ,总的边的数量级就是 \(O(Nlog_2N)\) ,边的级别显然大了很多,并且也不符合线段树的关系定义。
一个 \(8\) 个节点的线段树预处理之后的连边情况如图所示。图中边权全部为 \(0\)。
\(\\\)
然后一条连边就可以按照我们说的那样了,找到对应的区间就好。
假如 \(8\) 号点要向区间 \([2,5]\) 连边,最后会是蓝边的样子(边权就省略了):
\(\\\)
建图过程 (区间 \(\to\) 点)
发现原来线段树的区间传递关系并不对了。
如果我们找到了每一段区间,直接向指向的点连边,子树内真正建边的点并没有连出这一条边。
如图,如果要区间 \([5,8]\) 向 \(T\) 连边,蓝框框起来的点并没有真正向 \(T\) 连边。
因为建树的时候绿色边的方向是向区间内传递的。
\(\\\)
很容易想到反向再建一棵树。显然叶节点是相同的,所以两棵树会共用叶节点。
\(\\\)
此时在这颗倒着的树上传递关系就变为,一个节点连了出边,那么他所代表的区间里所有节点都向外连了一条出边。
同理,这些绿色的边的边权都是 \(0\) 。
关于正确性,显然更大的区间并不会到达小区间,所以连边不用担心范围超界。
此时我们就可以在倒着的树上找所有的区间,然后连出边就可以了。
\(\\\)
实现
其实做的时候共用叶节点并不好写(至少对于我这种封装数据结构的就很不友善)。
每个线段树节点维护一个 \(p\) 代表这个节点的编号,显然两棵树的叶节点对应的部分编号相同,其他部分两棵树的每一个节点都要重新编号。
首先是建树部分,注意两棵树的初始化边的方向并不同。
void build_in(ll &rt,ll l,ll r){
rt=newnode();
if(l==r){c[rt].p=l;return;}
build_in(c[rt].ls,l,mid);
build_in(c[rt].rs,mid+1,r);
c[rt].p=++cnt;
add(c[rt].p,c[c[rt].ls].p,0);
add(c[rt].p,c[c[rt].rs].p,0);
}
void build_out(ll &rt,ll l,ll r){
rt=newnode();
if(l==r){c[rt].p=l;return;}
build_out(c[rt].ls,l,mid);
build_out(c[rt].rs,mid+1,r);
c[rt].p=++cnt;
add(c[c[rt].ls].p,c[rt].p,0);
add(c[c[rt].rs].p,c[rt].p,0);
}
然后 \(updata\) 的时候需要分靠讨论。
如果是 点 \(\to\) 区间 连边,要在第一棵树上找区间,然后向内连入边,也就是出发点向这个节点连边。
inline void updata(ll rt,ll l,ll r,ll L,ll R,ll p,ll w){
if(l>R||r<L) return;
if(l>=L&&r<=R){add(p,c[rt].p,w);return;}
if(L<=mid) updata(c[rt].ls,l,mid,L,R,p,w);
if(R>mid) updata(c[rt].rs,mid+1,r,L,R,p,w);
}
反之要在第二棵树上找区间,然后向外连出边,也就是这个节点向终点连边。
inline void updata(ll rt,ll l,ll r,ll L,ll R,ll p,ll w){
if(l>R||r<L) return;
if(l>=L&&r<=R){add(c[rt].p,p,w);return;}
if(L<=mid) updata(c[rt].ls,l,mid,L,R,p,w);
if(R>mid) updata(c[rt].rs,mid+1,r,L,R,p,w);
}
然后该跑什么图论算法就跑什么图论算法就好了。
\(\\\)
我们以 [ CodeForces 786 B ] Legacy 为例提供一份模板。
#include<cmath>
#include<queue>
#include<cstdio>
#include<cctype>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#define N 200010
#define gc getchar
#define Rg register
#define mid ((l+r)>>1)
#define inf 900000000000000000ll
using namespace std;
typedef long long ll;
inline ll rd(){
ll x=0; bool f=0; char c=gc();
while(!isdigit(c)){if(c=='-')f=1;c=gc();}
while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=gc();}
return f?-x:x;
}
ll n,m,s,cnt,tot,hd[N<<2];
struct edge{ll w,to,nxt;}e[N*10];
inline void add(ll u,ll v,ll w){
e[++tot].to=v; e[tot].w=w;
e[tot].nxt=hd[u]; hd[u]=tot;
}
struct segmentin{
ll root,ptr;
inline ll newnode(){return ++ptr;}
struct node{ll ls,rs,p;}c[N<<1];
void build(ll &rt,ll l,ll r){
rt=newnode();
if(l==r){c[rt].p=l;return;}
build(c[rt].ls,l,mid);
build(c[rt].rs,mid+1,r);
c[rt].p=++cnt;
add(c[rt].p,c[c[rt].ls].p,0);
add(c[rt].p,c[c[rt].rs].p,0);
}
inline void updata(ll rt,ll l,ll r,ll L,ll R,ll p,ll w){
if(l>R||r<L) return;
if(l>=L&&r<=R){add(p,c[rt].p,w);return;}
if(L<=mid) updata(c[rt].ls,l,mid,L,R,p,w);
if(R>mid) updata(c[rt].rs,mid+1,r,L,R,p,w);
}
}treein;
struct segmentout{
ll root,ptr;
inline ll newnode(){return ++ptr;}
struct node{ll ls,rs,p;}c[N<<1];
void build(ll &rt,ll l,ll r){
rt=newnode();
if(l==r){c[rt].p=l;return;}
build(c[rt].ls,l,mid);
build(c[rt].rs,mid+1,r);
c[rt].p=++cnt;
add(c[c[rt].ls].p,c[rt].p,0);
add(c[c[rt].rs].p,c[rt].p,0);
}
inline void updata(ll rt,ll l,ll r,ll L,ll R,ll p,ll w){
if(l>R||r<L) return;
if(l>=L&&r<=R){add(c[rt].p,p,w);return;}
if(L<=mid) updata(c[rt].ls,l,mid,L,R,p,w);
if(R>mid) updata(c[rt].rs,mid+1,r,L,R,p,w);
}
}treeout;
bool vis[N<<2];
ll dis[N<<2];
priority_queue<pair<ll,ll> >q;
inline void dij(){
for(Rg ll i=1;i<=(n<<2);++i) dis[i]=inf;
dis[s]=0; q.push(make_pair(0,s));
while(!q.empty()){
ll u=q.top().second; q.pop();
if(vis[u]) continue; vis[u]=1;
for(Rg ll i=hd[u],v;i;i=e[i].nxt)
if(dis[v=e[i].to]>dis[u]+e[i].w){
dis[v]=dis[u]+e[i].w;
q.push(make_pair(-dis[v],v));
}
}
}
int main(){
cnt=n=rd(); m=rd(); s=rd();
treein.build(treein.root,1,n);
treeout.build(treeout.root,1,n);
for(Rg ll i=1,op,u,l,r,w;i<=m;++i){
op=rd();
if(op==1){
l=rd(); r=rd();
w=rd(); add(l,r,w);
}
else if(op==2){
u=rd(); l=rd(); r=rd(); w=rd();
treein.updata(treein.root,1,n,l,r,u,w);
}
else{
u=rd(); l=rd(); r=rd(); w=rd();
treeout.updata(treeout.root,1,n,l,r,u,w);
}
}
dij();
for(Rg ll i=1;i<=n;++i)
printf("%lld ",dis[i]<inf?dis[i]:-1);
return 0;
}
\(\\\)
一道例题
一张 \(N\) 个点的 无向图 ,边以以下 \(M\) 条信息的形式给出:
- \(l_1\ r_1\ l_2\ r_2\) 代表区间 \([l_1,r_1]\) 内的每一个点与区间 \([l_2,r_2]\) 内的每一个点之间都有一条长度为 \(1\) 的无向边。
求 \(S\) 点的单源最短路,保证图连通。
- \(N\le 5\times10^5,M\le 10^5,1\le l_1,l_2,r_1,r_2\le N\)
\(\\\)
就是把问题放到了区间向区间连边上。
朴素的想法是两侧都找出对应的若干区间,然后枚举两侧的每一个区间构成的 \(pair\) ,然后对应连边。
实际上并不需要这样。
我们对每一条信息都单独开出来一个转移节点,然后就变成了 区间 \(\to\) 点和 点 \(\to\) 区间的问题了。
但是双向边不能共用一个转移节点。因为这样其实上在 \([l_1,r_1]\) 内的每个点之间都连了一条长度为 \(2\) 的边。
实际做的时候,对每一个单向边都开一个转移节点,然后出边边权都设为 \(0\) ,入边边权都设为 \(1\) 就可以了。
还有一个比较妙的方法,是出入边权都设为 \(1\) 然后最短路的答案除以 \(2\) ,也是可以的。
\(\\\)
n#include<cmath>
#include<queue>
#include<cstdio>
#include<cctype>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#define N 1000010
#define M 4000010
#define gc getchar
#define Rg register
#define mid ((l+r)>>1)
using namespace std;
inline int rd(){
int x=0; bool f=0; char c=gc();
while(!isdigit(c)){if(c=='-')f=1;c=gc();}
while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=gc();}
return f?-x:x;
}
int n,m,s,cnt,tot,hd[N<<3];
struct edge{int w,to,nxt;}e[M<<1];
inline void add(int u,int v,int w){
e[++tot].to=v; e[tot].w=w;
e[tot].nxt=hd[u]; hd[u]=tot;
}
struct segmentin{
int root,ptr;
inline int newnode(){return ++ptr;}
struct node{int ls,rs,p;}c[N<<1];
void build(int &rt,int l,int r){
rt=newnode();
if(l==r){c[rt].p=l;return;}
build(c[rt].ls,l,mid);
build(c[rt].rs,mid+1,r);
c[rt].p=++cnt;
add(c[rt].p,c[c[rt].ls].p,0);
add(c[rt].p,c[c[rt].rs].p,0);
}
void updata(int rt,int l,int r,int L,int R,int p,int w){
if(l>R||r<L) return;
if(l>=L&&r<=R){add(p,c[rt].p,w);return;}
if(L<=mid) updata(c[rt].ls,l,mid,L,R,p,w);
if(R>mid) updata(c[rt].rs,mid+1,r,L,R,p,w);
}
}treein;
struct segmentout{
int root,ptr;
inline int newnode(){return ++ptr;}
struct node{int ls,rs,p;}c[N<<1];
void build(int &rt,int l,int r){
rt=newnode();
if(l==r){c[rt].p=l;return;}
build(c[rt].ls,l,mid);
build(c[rt].rs,mid+1,r);
c[rt].p=++cnt;
add(c[c[rt].ls].p,c[rt].p,0);
add(c[c[rt].rs].p,c[rt].p,0);
}
void updata(int rt,int l,int r,int L,int R,int p,int w){
if(l>R||r<L) return;
if(l>=L&&r<=R){add(c[rt].p,p,w);return;}
if(L<=mid) updata(c[rt].ls,l,mid,L,R,p,w);
if(R>mid) updata(c[rt].rs,mid+1,r,L,R,p,w);
}
}treeout;
int dis[N<<3];
bool vis[N<<3];
priority_queue<pair<int,int> >q;
inline void dij(){
memset(dis,0x3f,sizeof(dis));
dis[s]=0; q.push(make_pair(0,s));
while(!q.empty()){
int u=q.top().second; q.pop();
if(vis[u]) continue; vis[u]=1;
for(Rg int i=hd[u],v;i;i=e[i].nxt)
if(dis[v=e[i].to]>dis[u]+e[i].w){
dis[v]=dis[u]+e[i].w;
q.push(make_pair(-dis[v],v));
}
}
}
int main(){
cnt=n=rd(); m=rd(); s=rd();
treein.build(treein.root,1,n);
treeout.build(treeout.root,1,n);
for(Rg int i=1,l1,r1,l2,r2;i<=m;++i){
++cnt;
l1=rd(); r1=rd();
l2=rd(); r2=rd();
treein.updata(treein.root,1,n,l1,r1,cnt,1);
treein.updata(treein.root,1,n,l2,r2,cnt,1);
treeout.updata(treeout.root,1,n,l1,r1,cnt,1);
treeout.updata(treeout.root,1,n,l2,r2,cnt,1);
}
dij();
for(Rg int i=1;i<=n;++i) printf("%d\n",dis[i]/2);
return 0;
}