DS 优化建图

_lgswdn

2020-08-20 21:08:28

Personal

注:还有很多建图,本文章没有涉及到,毕竟数据结构范围很大,但是作者个人认为比较常有的建图以及其思想这里都出现了,有兴趣的读者可以自行查找的未提及的优化建图,比如根号优化,KD优化,主席树优化等题目去尝试(本质和线段树优化的思想没有太大区别 qwq)。 并且如果有typo啥的请直接洛谷私信,这篇博客的评论翻起来太麻烦了。 没有任何图片炸了。 --- 我们从一道模板题开始优化建图之旅。 ## [CF786B Legacy](https://www.luogu.com.cn/problem/CF786B) ### 题目大意 有 $n$ 个点的有向图,有 $3$ 种边,分别为 - $u\to v$ 边权为 $w$ - $u\to $ $[l,r]$ 的所有点 边权为 $w$ - $[l,r]$ 的所有点 $\to u$ 边权为 $u$ 求从 $s$ 号点到所有点的单源最短路。 数据范围:$n,q\le 10^5$ ### 暴力 如果你不知道线段树优化建图的技巧,那么假如考场上遇到了这道题目,一个非常好做的部分分就是 $O(n^2\log n)$ 的暴力最短路。按照题目的意思把所有边都连了(也就是 $u\to [l,r]$ 时 $u$ 和 $[l,r]$ 的所有点都连上),然后跑一个 dijkstra。 对于 $n,q\le 1000$ 还是很稳的,然而对于 $n,q\le 10^5$ 的这个数据,你不 T 飞那么我就把这篇博客吃下去。 ### 线段树优化建图 瓶颈在于 $u\to [l,r]$ 的连边和 $[l,r]\to u$ 的连边。我们既然不能一个一个连,我们说不定可以把 $[l,r]$ 按照某种规律**拆分**成 $\log$ 级别的边数。我们很自然地想到了 **线段树**。 先拿一个线段树过来。 ![image.png](https://i.loli.net/2020/08/20/bDK1drAPmyScLeO.png) 对于第二个连边操作,$u\to [l,r]$,我们只需要从原图种的 $u$ 连向 $[l,r]$ 对应的线段树上的点就可以了。于是 $O(n)$ 的边数优化成了 $O(\log n)$。(下图例子为 $4\to [1,3]$)。 ![image.png](https://i.loli.net/2020/08/20/rvPSwxLpzVJcZlC.png) (绿色边边权为 $w$,蓝色边边权为 $0$)。 但是这道题还可以区间连向点,于是我们再建立一棵线段树,方向为根向,然后相同的方法连边。(下图例子为 $[1,3]\to 4$。 ![image.png](https://i.loli.net/2020/08/20/lYJb2ndquoSm1t9.png) 综合起来是这样的。 ![image.png](https://i.loli.net/2020/08/20/sY7txHgRSzcUZyE.png) 最后一个问题,每棵树的叶节点其实就是绿色节点。你可以选择合并叶节点和绿色节点,也可以选择直接连 $0$ 权无向边。我选择后者因为更加方便一点。 ![image.png](https://i.loli.net/2020/08/20/2UhwFvnLGsuPjfp.png) 这个建树用 zkw 更加方便一点,但是我不会,所以就递归弄。 代码中的一些规定:第一棵线段树编号为 $p$,第二棵线段树编号为 $p+4n$,然后绿色节点 $x$ 在图中的编号为 $x+8n$,这样每一个点的编号都不会相同了。 ```cpp #include<bits/stdc++.h> #define int long long using namespace std; const int N=9e5+9, M=5e6+9; //2棵线段树+普通节点 struct edge {int to,nxt,w;}e[M]; int hd[N],tot; void add(int u,int v,int w) {e[++tot]=(edge){v,hd[u],w};hd[u]=tot;} int n,q,s; struct Node {int l,r;}t[N]; void build(int p,int l,int r) { t[p].l=l, t[p].r=r; if(l==r) { add(l+8*n,p,0), add(p+4*n,l+8*n,0); add(p,l+8*n,0), add(l+8*n,p+4*n,0); return; } add(p,p*2,0), add(p*2+n*4,p+n*4,0); add(p,p*2+1,0), add(p*2+1+n*4,p+n*4,0); build(p*2,l,(l+r)/2), build(p*2+1,(l+r)/2+1,r); } void addh(int p,int x,int y,bool type,int u,int w) { int l=t[p].l, r=t[p].r, mid=((l+r)>>1); if(l==x&&y==r) { if(!type) return add(u+8*n,p,w); else return add(p+4*n,u+8*n,w); } if(y<=mid) addh(p*2,x,y,type,u,w); else if(x>mid) addh(p*2+1,x,y,type,u,w); else addh(p*2,x,mid,type,u,w), addh(p*2+1,mid+1,y,type,u,w); } int d[N]; namespace ShortestPath{ bool vst[N]; struct Node { int u,w; bool operator < (const Node &a) const { return w>a.w; } }; void dijkstra() { memset(d,0x3f,sizeof(d)); d[s]=0; priority_queue<Node>q; q.push((Node){s,0ll}); while(!q.empty()) { int u=q.top().u; q.pop(); if(vst[u]) continue; vst[u]=1; for(int i=hd[u],v;i;i=e[i].nxt) { if(!vst[v=e[i].to]&&d[v]>d[u]+e[i].w) { d[v]=min(d[v],d[u]+e[i].w); q.push((Node){v,d[v]}); } } } } } signed main() { scanf("%lld%lld%lld",&n,&q,&s); build(1,1,n); for(int i=1,op,v,u,w,l,r;i<=q;i++) { scanf("%lld",&op); if(op==1) scanf("%lld%lld%lld",&v,&u,&w), add(v+8*n,u+8*n,w); else if(op==2) scanf("%lld%lld%lld%lld",&v,&l,&r,&w), addh(1,l,r,0,v,w); else if(op==3) scanf("%lld%lld%lld%lld",&v,&l,&r,&w), addh(1,l,r,1,v,w); } s+=8*n; ShortestPath::dijkstra(); for(int i=1;i<=n;i++) if(d[i+8*n]<2e18) printf("%lld ",d[i+8*n]); else printf("-1 "); return 0; } ``` 运用 zkw 线段树然后不用绿点会获得常数优化,不过这题并不需要。 ## [[PA2011]Journeys](https://www.luogu.com.cn/problem/P6348) ### 题目大意 > $n$ 个点,每个连边为 $[a,b]\to [c,d]$,边权为 $1$。求最短路。 $n\le 5\times 10^5, m\le 10^5$ 这道题和前面这题一样,甚至还更加简单一点,但是如果直接连,那么边数复杂度是 $O(m\log^2 n)$ 的。所以我们还需要每次建两个虚点,比如说假设有一条 $[2.4]\to [1,3]$ 的边,那么我们可以这样建。 ![image.png](https://i.loli.net/2020/08/21/iBgR1Ln3U25mtP8.png) 双向边很好处理,我们把它拆成两个单向边就可以了。 绿边为 $1$ 权边,蓝边为 $0$ 权边,然后跑双端队列 BFS 即可。 ```cpp int n,m,s,id[N],cnt; struct Node {int l,r;}t[N]; void build(int p,int l,int r) { t[p].l=l, t[p].r=r; if(l==r) { id[l]=p; return; } add(p,p*2,0), add(p*2+n*4,p+n*4,0); add(p,p*2+1,0), add(p*2+1+n*4,p+n*4,0); build(p*2,l,(l+r)/2), build(p*2+1,(l+r)/2+1,r); } void adds(int p,int x,int y,bool type,int u) { int l=t[p].l, r=t[p].r, mid=((l+r)>>1); if(l==x&&y==r) { if(!type) return add(u,p,0); else return add(p+4*n,u,0); } if(y<=mid) adds(p*2,x,y,type,u); else if(x>mid) adds(p*2+1,x,y,type,u); else adds(p*2,x,mid,type,u), adds(p*2+1,mid+1,y,type,u); } int d[N]; void bfs() { deque<int>q; q.push_front(s); memset(d,0x3f,sizeof(d)); d[s]=0; while(!q.empty()) { int u=q.front(); q.pop_front(); for(int i=hd[u],v;i;i=e[i].nxt) if(d[v=e[i].to]>d[u]+e[i].w) { d[v]=d[u]+e[i].w; if(!e[i].w) q.push_front(v); else q.push_back(v); } } } signed main() { scanf("%d%d%d",&n,&m,&s); build(1,1,n); cnt=8*n; for(int i=1,a,b,c,dd;i<=m;i++) { scanf("%d%d%d%d",&a,&b,&c,&dd); int x=++cnt, y=++cnt; add(y,x,1), adds(1,c,dd,0,x), adds(1,a,b,1,y); x=++cnt, y=++cnt; add(y,x,1), adds(1,a,b,0,x), adds(1,c,dd,1,y); } for(int i=1;i<=n;i++) add(id[i],id[i]+4*n,0), add(id[i]+4*n,id[i],0); s=id[s]+4*n; bfs(); for(int i=1;i<=n;i++) printf("%d\n",d[id[i]]); return 0; } ``` ## [BZOJ4276 [ONTAK2015]Bajtman i Okrągły Robin](https://darkbzoj.tk/problem/4276) ### 题目大意 有 $n$ 个强盗,每个强盗会在时间段 $[l,r]$ 中选择一个长为 $1$ 的时间段来抢劫,且每个强盗有一个收益值 $a_i$。每一个长为 $1$ 的时间段,警察只可以抓住一个强盗并获得这个强盗带来的收益。问最多可能拿到多少收益。 $n,l,r\le 5000$。 ### 暴力 这是一个非常一眼的费用流(二分图最大权匹配)问题,如果不会的话左转去学费用流。所以暴力不多讲了。但是就算你费用流跑的再快,这个 $O(n^2)$ 的边数还是得让你直接爆炸。我们考虑优化建图。 ### 线段树优化建图 我们还是画一下心爱的线段树。我们看一下样例。 ``` 4 1 5 40 2 5 10 2 4 30 1 4 20 ``` 对于样例,我们很容易连边(非常 trivial,绿色强盗蓝色线段树)。 ![image.png](https://i.loli.net/2020/08/20/DuKXAWeQmfRGq7L.png) (还有一点,如果你这样建图且常数比较大的话有可能会 TLE,优化方法很简单,就像下面的代码一样,把叶节点向汇点连的边转化为根节点向汇连边,叶向树转化为根向树即可) 然后到此你就能把这道题做出来了。但是你可能有疑问,为什么这样就会有这么多优化呢?我们大致来看一个模型。 ![image.png](https://i.loli.net/2020/08/20/SOATRtcm9FkvCuN.png) 这是利用“中间商” $[1,2]$ 去建图后的图。 ![image.png](https://i.loli.net/2020/08/20/iLtpa4JhbNMeVBT.png) 这是暴力建图。 我们可以看到,对于每一个完全二分图,我们把边数从乘的关系变成了加的关系,相当于把一些流先存储在 $[1,2]$ 然后再去分发给 $1$ 和 $2$。 而对于线段树上的每一个类似于“中间商”的节点,都起到了存储的作用,而为什么是 $\log$ 呢?因为线段树的高度是 $\log$,也就是说每一个流会经过 $O(\log)$ 个中间商,所以便是 $O(n\log n)$ 级别的边数。 接下来放上这题的代码。 ```cpp namespace MCMF { struct Edge {int to,nxt,c,w;}e[N*2]; int hd[N],tot; void add(int u,int v,int c,int w) {e[++tot]=(Edge){v,hd[u],c,w};hd[u]=tot;} #define debug printf("%d %d %d %d\n",u,v,c,w) void addh(int u,int v,int c,int w) {add(u,v,c,w), add(v,u,0,-w);} int s,t,cost,d[N]; bool in[N]; bool spfa() { queue<int>q; q.push(s); memset(d,0x3f,sizeof(d)); d[s]=0; while(!q.empty()) { int u=q.front(); q.pop(), in[u]=0; for(int i=hd[u],v;i;i=e[i].nxt) { if(!e[i].c) continue; if(d[v=e[i].to]>d[u]+e[i].w) { d[v]=d[u]+e[i].w; if(!in[v]) q.push(v), in[v]=1; } } } return d[t]<inf; } int dinic(int u,int flow) { if(u==t) return flow; int rest=flow; in[u]=1; for(int i=hd[u],v;i&&rest;i=e[i].nxt) if(!in[v=e[i].to]&&e[i].c&&d[v]==d[u]+e[i].w) { int use=dinic(v,min(e[i].c,rest)); if(!use) d[v]=-1; rest-=use, e[i].c-=use, e[i^1].c+=use, cost+=use*e[i].w; } in[u]=0; return flow-rest; } void clear() { memset(e,0,sizeof(e)), memset(hd,0,sizeof(hd)); tot=1; cost=0; memset(in,0,sizeof(in)); } int flow(int ss,int tt) { s=ss, t=tt; while(spfa()) while(dinic(s,inf)); return cost; } } int n,ss,tt,num; int l[N],r[N],a[N]; struct Node {int l,r;}t[N]; void build(int p,int l,int r) { t[p].l=l, t[p].r=r; if(l==r) return; int mid=(l+r)/2; build(p*2,l,mid), build(p*2+1,mid+1,r); MCMF::addh(p*2,p,mid-l+1,0), MCMF::addh(p*2+1,p,r-mid,0); } void adds(int p,int l,int r,int u) { if(t[p].l==l&&t[p].r==r) return MCMF::addh(u+20000,p,1,-a[u]); int mid=(t[p].l+t[p].r)/2; if(r<=mid) adds(p*2,l,r,u); else if(l>mid) adds(p*2+1,l,r,u); else adds(p*2,l,mid,u), adds(p*2+1,mid+1,r,u); } int main() { scanf("%d",&n); MCMF::clear(); ss=30000+1, tt=30000+2; int maxr=0; for(int i=1;i<=n;i++) scanf("%d%d%d",&l[i],&r[i],&a[i]), r[i]--, maxr=max(maxr,r[i]); build(1,1,maxr); MCMF::addh(1,tt,maxr,0); for(int i=1;i<=n;i++) MCMF::addh(ss,i+20000,1,0); for(int i=1;i<=n;i++) adds(1,l[i],r[i],i); printf("%d\n",-MCMF::flow(ss,tt)); return 0; } ``` ## 不止是线段树 有一些题目,它并不能用线段树做多少事情,但是却可以用类似的想法,建造一个数据结构,然后减少边的数量。比如这道题。 Walk (你们可以搜到这道题,但我找不到哪里可以提交的 OJ)。 ### 题意描述 > 在比特镇一共有 $n$ 个街区,编号依次为 $1$ 到 $n$,它们之间通过若干条单向道路连接。比特镇的交通系统极具特色,除了 $m$ 条单向道路之外,每个街区还有一个编码 $val_i$,不同街区可能拥有相同的编码。如果 $val_i \text{ and } val_j = val_j$,那么也会存在一条额外的从 $i$ 出发到 $j$ 的单向道路。 >Byteasar 现在位于 $1$ 号街区,他想知道通过这些道路到达每一个街区最少需要多少时间。因为比特镇的交通十分发达,你可以认为通过每条道路都只需要 1 单位时间。 >$n,m\le 300000, val\le 2^{20}$。 ### 优化建图 暴力非常显然,把边全部建出来,图是建好了,评测结果便 T 飞了。 既然题目中考虑二进制 $\text{and}$ 操作,那么我们拆位看看。我们假设最大的 $val$ 不超过 $(111)_2$,并且我们把所有的 $val$ 按照每一个二进制数中含有的 $1$ 的个数来进行整理。 ![image.png](https://i.loli.net/2020/08/20/M6S5kUl2nF3KByX.png) 然后我们再自己创造一个样例。假设这些点的 $val$ 为 $\{7,2,5,3,0\}$。我们暴力建边是这样的。 ![image.png](https://i.loli.net/2020/08/20/AWw6TGgXa9ScHy1.png) 我们发现有很多冗余的边。冗余的边主要来自于 **$i\text { and } j=j$** 的所有连边其实是个传递闭包。是什么传递闭包呢?是这张图的传递闭包。 ![image.png](https://i.loli.net/2020/08/21/tH3ifq6QICEMpx1.png) 所以实际上我们并不需要 $O(n^2)$ 的连边,因为上图已经足以表示对于所有满足 $i\text { and } j=j$ 的 $(i,j)$ 存在一条 $i\to j$ 的路径。我们按照以前的思想,把原图搬到这张图上。 ![image.png](https://i.loli.net/2020/08/21/bY5ohgerRDS7HnW.png) 于是大功告成!对于 $m$ 条单向道路,直接在绿点上建即可。蓝边的边数为 $O(val\log (val))$,绿边边数为 $O(n+m)$,其中蓝边为无权有向边,绿边为有权边,然后建图跑双端队列 BFS 即可。 ```cpp #include<bits/stdc++.h> using namespace std; const int N=2e6+9, M=1e7+2e6+9, base=(1<<20); struct edge {int to,nxt,w;}e[M]; int hd[N],tot; void add(int u,int v,int w) {e[++tot]=(edge){v,hd[u],w};hd[u]=tot;} int n,m,d[N]; void bfs() { deque<int>q; q.push_front(1+base); memset(d,0x3f,sizeof(d)); d[1+base]=0; while(!q.empty()) { int u=q.front(); q.pop_front(); for(int i=hd[u],v;i;i=e[i].nxt) if(d[v=e[i].to]>d[u]+e[i].w) { d[v]=d[u]+e[i].w; if(!e[i].w) q.push_front(v); else q.push_back(v); } } } int main() { scanf("%d%d",&n,&m); for(int i=0;i<=base;i++) { for(int j=0;j<=21;j++) if((1<<j)&i) add(i,i^(1<<j),0); } for(int i=1,v;i<=n;i++) scanf("%d",&v), add(i+base,v,0), add(v,i+base,1); for(int i=1,u,v;i<=m;i++) scanf("%d%d",&u,&v), add(u+base,v+base,1); bfs(); for(int i=1;i<=n;i++) printf("%d ",(d[i+base]>1e9? -1 : d[i+base])); return 0; } ```