线段树优化建图 学习笔记

\(\\\)

适用情况


当点数很多,并且边集以区间形式 \((\)\(\to\) 区间 \(/\) 区间 \(\to\)\()\) 给出时,我们发现强行连边复杂度太大,考虑如何维护同一个子区间的连边,容易想到线段树。

\(\\\)

优化思路


我们先考虑 点 \(\to\) 区间 连边的部分。

考虑建图的复杂度真正体现在哪里。

如果所有的被连边区间不交,那么总复杂度是对的。

所以真正建图的复杂度,产生在被连边区间相交的部分,尤其是一个同一个区间被多个点连边,那么我们每一个都要产生区间长度的代价。

那么有一个思路,如果有一个区间被多次连边,那么不妨我们建立这个区间的一个代表节点。

首先预处理的时候将这个代表节点连向区间里的每一个节点,边权为 \(0\) ,就代表了连向代表节点的每一条边都连向了区间里的每一个节点,然后都每次都将边连向这个代表节点即可。

\(\\\)

建图过程 (点 \(\to\) 区间)


但是注意到这种区间并不是很好找,这种找区间的方法并不是具有很强的普适性。

我们知道线段树的每一个节点维护的是一个固定的区间,线段树的区间操作是 \(log\) 的。

回忆区间加的过程,如果一个节点的代表区间被完全覆盖,那么我们会在这个节点上打标记,等到需要的时候再下传。这体现了一种代表的关系,即区间共有的性质我可以只标记在代表这个区间的节点上。

于是有了一个思路,如果要从一个点连向一个区间,那么可以像区间加那样,把这个点向完全覆盖的区间连边。

也就是说,在区间连边时,在线段树上每找到一个完全覆盖的节点,就将出发点向这个区间的代表节点连边。


同样的,我们先考虑预处理。

一个节点被连边,代表这个节点的 子树 都可以到达。

之所以强调子树,而不是强调区间里的每一个节点,因为我们预处理的边只会向下 连一层

只连下一层的边数量级是 \(O(2N)\) ,而连向区间里的每一个节点显然每一层都是 \(O(N)\) ,总的边的数量级就是 \(O(Nlog_2N)\) ,边的级别显然大了很多,并且也不符合线段树的关系定义。

一个 \(8\) 个节点的线段树预处理之后的连边情况如图所示。图中边权全部为 \(0\)

5be9808a3cb4b.png

\(\\\)

然后一条连边就可以按照我们说的那样了,找到对应的区间就好。

假如 \(8\) 号点要向区间 \([2,5]\) 连边,最后会是蓝边的样子(边权就省略了):

5be9806ebbcd8.png

\(\\\)

建图过程 (区间 \(\to\) 点)


发现原来线段树的区间传递关系并不对了。

如果我们找到了每一段区间,直接向指向的点连边,子树内真正建边的点并没有连出这一条边。

如图,如果要区间 \([5,8]\)\(T\) 连边,蓝框框起来的点并没有真正向 \(T\) 连边。

因为建树的时候绿色边的方向是向区间内传递的。

5be9804a1c26d.png

\(\\\)

很容易想到反向再建一棵树。显然叶节点是相同的,所以两棵树会共用叶节点。

5be97fb9ec90a.png

\(\\\)

此时在这颗倒着的树上传递关系就变为,一个节点连了出边,那么他所代表的区间里所有节点都向外连了一条出边。

同理,这些绿色的边的边权都是 \(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;
}

转载于:https://www.cnblogs.com/SGCollin/p/9949287.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值