DS 优化建图

lgswdn_SA

2020-08-20 21:08:28

算法·理论

注:还有很多建图,本文章没有涉及到,毕竟数据结构范围很大,但是作者个人认为比较常有的建图以及其思想这里都出现了,有兴趣的读者可以自行查找的未提及的优化建图,比如根号优化,KD优化,主席树优化等题目去尝试(本质和线段树优化的思想没有太大区别 qwq)。

并且如果有typo啥的请直接洛谷私信,这篇博客的评论翻起来太麻烦了。

没有任何图片炸了。

我们从一道模板题开始优化建图之旅。

CF786B Legacy

题目大意

n 个点的有向图,有 3 种边,分别为

求从 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 级别的边数。我们很自然地想到了 线段树

先拿一个线段树过来。

对于第二个连边操作,u\to [l,r],我们只需要从原图种的 u 连向 [l,r] 对应的线段树上的点就可以了。于是 O(n) 的边数优化成了 O(\log n)。(下图例子为 4\to [1,3])。


(绿色边边权为 w,蓝色边边权为 0)。

但是这道题还可以区间连向点,于是我们再建立一棵线段树,方向为根向,然后相同的方法连边。(下图例子为 [1,3]\to 4

综合起来是这样的。

最后一个问题,每棵树的叶节点其实就是绿色节点。你可以选择合并叶节点和绿色节点,也可以选择直接连 0 权无向边。我选择后者因为更加方便一点。

这个建树用 zkw 更加方便一点,但是我不会,所以就递归弄。

代码中的一些规定:第一棵线段树编号为 p,第二棵线段树编号为 p+4n,然后绿色节点 x 在图中的编号为 x+8n,这样每一个点的编号都不会相同了。

#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

题目大意

$n\le 5\times 10^5, m\le 10^5

这道题和前面这题一样,甚至还更加简单一点,但是如果直接连,那么边数复杂度是 O(m\log^2 n) 的。所以我们还需要每次建两个虚点,比如说假设有一条 [2.4]\to [1,3] 的边,那么我们可以这样建。

双向边很好处理,我们把它拆成两个单向边就可以了。

绿边为 1 权边,蓝边为 0 权边,然后跑双端队列 BFS 即可。

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

题目大意

n 个强盗,每个强盗会在时间段 [l,r] 中选择一个长为 1 的时间段来抢劫,且每个强盗有一个收益值 a_i。每一个长为 1 的时间段,警察只可以抓住一个强盗并获得这个强盗带来的收益。问最多可能拿到多少收益。

### 暴力 这是一个非常一眼的费用流(二分图最大权匹配)问题,如果不会的话左转去学费用流。所以暴力不多讲了。但是就算你费用流跑的再快,这个 $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; } ```