Loading

【学习笔记】网络流

Page Views Count

一些定义

  • 网络:一个有向图 \(G=(V,E)\),每条边都有一个容量 \(c(u,v)\),存在源点 \(s\) 与 汇点 \(t\)

  • 流:\(f(u,v)\) 为边 \((u,v)\) 上的流,满足 \(f(u,v)\le c(u,v)\)\(f(u,v)=-f(v,u)\)\(\forall x\in V \sum_{(u,x)\in E} f(u,x)=\sum_{(x,v)\int E} f(x,v)\)。即满足:容量限制、写对称性、流守恒性。
    形式化定义:

    \[f(u,v)=\begin{cases} f(u,v)&(u,v)\in E\\ -f(v,u)&(v,u)\in E\\ 0&(u,v)\notin E,(v,u)\notin E \end{cases} \]

  • 剩余容量:\(c(u,v)-f(u,v)\)

  • 残量网络:剩余容量组成的网络

最大流与最小割

增广路算法与 EK 算法

先考虑一个假做法:每次搜索,能加流量就加。

这个算法的问题在于, \((u,v),(v,w),(x,w),(w,y)\) 四条边中 \((w,y)\) 流满时,可以让 \((u,v),(v,w)\) 带来的贡献尽量的小而让 \((u,v)\) 能流向其他节点。

简洁地说:我们需要反悔贪心。

于是构建反向边,初始容量为 \(0\),每次找到一条增广路( \(s\)\(t\) 容量均不为 \(0\))的路径,就将路径上所有边容量减少,反向对应增加,就达到的反悔的效果。

一个简单的优化:每次只增广当前的最短增广路,对于每条边来说,其被流满再被反悔时,最短路一定增加,于是增广上界是 \(O(nm)\),而 bfs 找最短路是 \(O(m)\) 的,复杂度 \(O(nm^2)\),上界要多松有多松。这就是 Edmonds-Karp 算法。

点击查看代码
int n,m,S,T;
struct Graph{
    struct edge{
        int to,nxt;
        ll lim;
    }e[maxm<<2];
    int head[maxn],cnt=1;
    inline void add_edge(int u,int v,int w){
        e[++cnt].to=v,e[cnt].nxt=head[u],e[cnt].lim=w,head[u]=cnt;
        e[++cnt].to=u,e[cnt].nxt=head[v],e[cnt].lim=0,head[v]=cnt;
    }
    ll now_flow[maxn];
    int pre[maxn];
    inline ll max_flow(int S,int T){
        ll flow=0;
        while(1){
            queue<int> q;
            memset(now_flow,-1,sizeof(now_flow));
            now_flow[S]=maxxn;
            q.push(S);
            while(!q.empty()){
                int u=q.front();
                q.pop();
                for(int i=head[u];i;i=e[i].nxt){
                    int v=e[i].to;
                    if(e[i].lim&&now_flow[v]==-1){
                        now_flow[v]=min(now_flow[u],e[i].lim);
                        pre[v]=i;
                        q.push(v);
                    }
                }
            }
            if(now_flow[T]==-1) return flow;
            flow+=now_flow[T];
            for(int u=T;u!=S;u=e[pre[u]^1].to){
                e[pre[u]].lim-=now_flow[T];
                e[pre[u]^1].lim+=now_flow[T];
            }
        }
    }
}G;

Dinic 算法

由于最短路只有 \(O(n)\) 级别,能不能每次都直接将所有当前最短路都增广以达到优化的目的?使用 dfs 但不能打标记,这是指数级的,不过有很多无用的遍历出现在已经流满的边,于是使用当前弧优化和删点优化,每次只搜索可能增广的边。注意这里的优化是对于单次 dfs 的,也就是不会影响下一次对残量网络的最短路的增广,同时当前最短路一定是个分层图(甚至可以理解成树),得到 Dinic 算法。

点击查看代码
int n,m,S,T;
struct Graph{
    struct edge{
        int to,nxt,lim;
    }e[maxm<<1];
    int head[maxn],cnt=1,cur[maxn];
    inline void add_edge(int u,int v,int w){
        e[++cnt].to=v,e[cnt].nxt=head[u],e[cnt].lim=w,head[u]=cnt;
        e[++cnt].to=u,e[cnt].nxt=head[v],e[cnt].lim=0,head[v]=cnt;
    }
    int dis[maxn];
    ll dfs(int u,ll rest){
        if(u==T) return rest;
        ll flow=0;
        for(int i=cur[u];i&&rest;i=e[i].nxt){
            //当前弧优化
            cur[u]=i;
            int v=e[i].to,c=min(rest,(ll)e[i].lim);
            if(dis[u]+1==dis[v]&&c){
                ll k=dfs(v,c);
                flow+=k,rest-=k;
                e[i].lim-=k,e[i^1].lim+=k;
            }
        }
        //删点优化
        if(!flow) dis[u]=-1;
        return flow;
    }
    //所有的优化都是对单次dfs有效,是在优化dfs,对整个算法流程没有正确性的影响
    inline ll max_flow(int S,int T){
        ll flow=0;
        //对残量网络跑bfs最短路
        while(1){
            queue<int> q;
            memcpy(cur,head,sizeof(head));
            memset(dis,-1,sizeof(dis));
            dis[S]=0;
            q.push(S);
            while(!q.empty()){
                int u=q.front();
                q.pop();
                for(int i=head[u];i;i=e[i].nxt){
                    int v=e[i].to;
                    if(dis[v]==-1&&e[i].lim){
                        dis[v]=dis[u]+1;
                        q.push(v);
                    }
                }
            }
            if(dis[T]==-1) return flow;
            flow+=dfs(S,maxxn);
        }
    }
}G;

最大流模型构建

网络流算法与实现并不难理解,主要是建模。

  • 点权化边权

    流量实际上是对次数的限制,而当我们要求某个点的流量受限时,将其拆成两个点,中间连边的边权即为限制的点权。

    例题:BZOJ-1066 SCOI 2007 蜥蜴

  • 求最小的问题

    求最小代价,最小用时等问题时有以下两种建模方法。

    一是考虑补集转化,即求最小代价改成求最大无需的代价,变成最大流模型。

    二是考虑二分,这种情况往往是要根据答案来构建模型,有流量的限制,不得不在已知答案的情况下判断是否合法。

    例题:BZOJ-1458 士兵占领BZOJ-1189 HNOI 2007 紧急疏散 evacuate

最大流最小割定理

最小割:割去一些边使得 \(s\)\(t\) 不连通,且割去的边容量之和最小

最大流等于最小割。

考虑对于每个流,一定存在一个关键边(被流满的)使得其不能再增广,那么割去这条边的容量就是割去的流量,得出最大流等与最小割。

一些图上概念与性质

  • 匹配:一个集合 \(E'\in E\) 使得 \(E'\) 中边两两无公共顶点点。

  • 独立集:一个集合 \(V'\in V\) 使得 \(V'\) 中点两两无公共边。

  • 边覆盖:一个集合 \(E'\in E\) 使得对于 \(u\in V\) 都存在一条以 \(u\) 为顶点的边。

  • 点覆盖:一个集合 \(V'\in V\) 使得对于 \(e\in E\) 的至少一个顶点出现在集合中。

  • \(|E_{\max}|+|E_{\min}|=n\)

    证明考虑在最大匹配基础上连边,有 \(n-2|E_{\max}|\) 个节点没有连边,每次连一条边就会增加一个节点有边覆盖,那么 \(|E_{\min}|=|E_{\max}|+n-2|E_{\max}|\),也就是:\(|E_{\max}|+|E_{\min}|=n\)

  • \(|V_{\max}|+|V_{\min}|=n\)

    可以证明一个更强的结论:独立集的补集是点覆盖,考虑独立集中两两无连边,也就是说独立集中非孤立点的连边一定在这个补集中,也就是这个补集构成了点覆盖,独立集最大时点覆盖最小。

  • 在二分图中,\(|E_{\max}|=|V_{\min}|\)

    考虑网络流建模,一侧点向源点,另一侧向汇点,容量为 \(1\),其余的边容量无限大。那么最大流一定是最大匹配,因为一个节点最多流 \(1\) 的流量,而最小割一定是割去向源点或汇点的连边,而对于每条二分图的边其左右两条边一定会被割去一条,也就是最小割,根据最大流最小割定理,可证:\(|E_{\max}|=|V_{\min}|\)

最小割模型构建

(无向图最小割正反向边初始容量都是原容量可以减少边数)

也是比较套路

  • 有叠加限制的问题

    可以是矩形中相邻,可以是同时选或不选会造成影响。

    将源点与汇点视作选与不选,割之后与源点相连的选,反之不选。这时两两连边就代表了限制,初始默认全部都选的情况,求容量要列出方程,考虑都选、都不选以及选择其一时断掉的边以及对应减少的贡献,可以解出方程,注意方程的解常规情况下都是使同类边意义或数值相等的,求出最小割即可。

    例题:BZOJ-2127 happiness

  • 出现负数或零

    在上一模型中,可能会出现正贡献与负贡献同时出现,或容量解出恰好是 \(0\),多数出现在与源点或汇点相连的边中,而这种边选取的个数是固定的,于是可以给一个附加权值,保证是正数。

    例题:BZOJ-3996 TJOI 2015 线性代数

  • 二分图模型

    对于不能共存的模型,连无限大的边表示不能共存,如果能保证二分图的性质,可以拆点后与源点汇点连权值的边,不共存的连无限大的边,由于要保证二分图,模型受限比较多。

    例题:BZOJ-3158 千钧一发

  • 离散变量模型

    遇到形如在每个序列中选取一个数,要求权值和最小且相邻序列选取元素的距离不能超过一个定值。

    如果没有距离限制可以直接把序列串成链跑最小割,有距离的限制就是不能割两个超过此距离的,只需要保证割完仍然连通,同上面叠加限制问题相似,向距离上界的点连一条容量无限大的边使得割去这两条边仍然连通,注意这里应当是从位置靠后的向考前的连边,可以画图理解。

    例题:BZOJ-3144 切糕

  • 最大权闭合子图模型(另一种叠加限制模型)

    将正贡献点连源点,负贡献点连汇点,中间连边表示一种限制关系(必须用同时选),这样最小割要么是牺牲正贡献,要么是接受负贡献。

    例题:BZOJ-1497 NOI 2006 最大获利BZOJ-1565 NOI 2009 植物大战僵尸BZOJ-4873 SHOI 2017 寿司餐厅

  • 平面图最小割转对偶图最短路

    把平面图每个边围成的极小封闭图形看作一个点,在 \(S\)\(T\) 之间,连一条边,这条边与原图围成的部分视作一个点,剩余部分视作另一个点。对于两个极小封闭图形之间的边,在构造的图上连一条边。这样求最小割就是在最短路上求刚刚增加的两个点之间最短路。

    无向图首先要拆成两条有向边,在网格图当中,常用顺时针法,即原图中边的方向顺时针旋转 \(90^{\circ}\) 得到对偶图边的方向。

    例题:BZOJ-2007 NOI 2010 海拔

最小割树

可以处理任意两点间最小割。

结合 Kruskal 重构树的思想,钦定两点 \(s,t\) 为源点汇点,求出最小割后的两个连通块 \(S,T\)\(x\in S,y\in T\),二者最小割一定是 \(\mathrm{mincut}(x,y)\le\mathrm{mincut}(s,t)\),反之 \(x,y\) 连通则 \(s,t\) 连通。

根据这个性质就可以每次选取两个仍然连通的点,跑最小割,在树中加入这条边,然后分成两个小连通块再分治,这样一来,两个点的最小割实际上就是在把他们不断分开的割中取最小值,就是树上路径问题了。

点击查看代码
int n,m,q;
struct Tree{
    struct edge{
        int v,w;
        edge()=default;
        edge(int v_,int w_):v(v_),w(w_){}
    };
    vector<edge> E[maxn];
    inline void add_edge(int u,int v,int w){
        E[u].push_back(edge(v,w));
        E[v].push_back(edge(u,w));
    }
    int fa[maxn],dep[maxn],siz[maxn],son[maxn],W[maxn];
    int top[maxn],dfn[maxn],dfncnt;
    int st[maxn][10];
    void dfs1(int u,int f,int d){
        fa[u]=f,dep[u]=d,siz[u]=1;
        int maxson=-1;
        for(edge e:E[u]){
            int v=e.v,w=e.w;
            if(v==f) continue;
            W[v]=w;
            dfs1(v,u,d+1);
            siz[u]+=siz[v];
            if(siz[v]>maxson) maxson=siz[v],son[u]=v;
        }
    }
    void dfs2(int u,int t){
        top[u]=t,dfn[u]=++dfncnt;
        st[dfn[u]][0]=W[u]?W[u]:1e9;
        if(!son[u]) return;
        dfs2(son[u],t);
        for(edge e:E[u]){
            int v=e.v;
            if(v==fa[u]||v==son[u]) continue;
            dfs2(v,v);
        }
    }
    inline void build_st(){
        for(int k=1;k<=9;++k){
            for(int i=1;i+(1<<k)-1<=n;++i){
                st[i][k]=min(st[i][k-1],st[i+(1<<k-1)][k-1]);
            }
        }
    }
    inline int query_st(int l,int r){
        int k=log2(r-l+1);
        return min(st[l][k],st[r-(1<<k)+1][k]);
    }
    inline int query(int u,int v){
        int res=1e9;
        while(top[u]!=top[v]){
            if(dep[top[u]]>dep[top[v]]) swap(u,v);
            res=min(res,query_st(dfn[top[v]],dfn[v]));
            v=fa[top[v]];
        }
        if(dep[u]>dep[v]) swap(u,v);
        if(u!=v) res=min(res,query_st(dfn[u]+1,dfn[v]));
        return res;
    }
}Tr;
pii res1,res2;
struct Graph{
    struct edge{
        int to,nxt,lim,tmp;
    }e[maxm<<1];
    int head[maxn],cnt=1;
    inline void add_edge(int u,int v,int w){
        e[++cnt].to=v,e[cnt].nxt=head[u],e[cnt].lim=w,e[cnt].tmp=w,head[u]=cnt;
        e[++cnt].to=u,e[cnt].nxt=head[v],e[cnt].lim=0,e[cnt].tmp=0,head[v]=cnt;
    }
    inline void init(){
        for(int i=1;i<=cnt;++i) e[i].lim=e[i].tmp;
    }
    int cur[maxn],dis[maxn];
    int dfs(int u,int T,int rest){
        if(u==T) return rest;
        int flow=0;
        for(int i=cur[u];i&&rest;i=e[i].nxt){
            cur[u]=i;
            int v=e[i].to,C=min(rest,e[i].lim);
            if(dis[u]+1==dis[v]&&C){
                int k=dfs(v,T,C);
                flow+=k,rest-=k;
                e[i].lim-=k,e[i^1].lim+=k;
            }
        }
        if(!flow) dis[u]=-1;
        return flow;         
    }
    int col[maxn],tot;
    inline int max_flow(int S,int T){
        int flow=0;
        while(1){
            queue<int> q;
            memcpy(cur,head,sizeof(head));
            memset(dis,-1,sizeof(dis));
            dis[S]=0;
            q.push(S);
            while(!q.empty()){
                int u=q.front();
                q.pop();
                for(int i=head[u];i;i=e[i].nxt){
                    int v=e[i].to;
                    if(dis[v]==-1&&e[i].lim){
                        dis[v]=dis[u]+1;
                        q.push(v);
                    }
                }
            }
            if(dis[T]==-1){
                res1=make_pair(S,0),res2=make_pair(T,0);
                ++tot;
                for(int i=1;i<=n;++i){
                    if(col[i]!=col[S]) continue;
                    if(dis[i]!=-1){
                        if(i!=S) res1.second=i;
                    }
                    else{
                        col[i]=tot;
                        if(i!=T) res2.second=i;
                    }
                }
                return flow;
            }
            flow+=dfs(S,T,1e9);
        }
    }
}G;
inline void build(pii node){
    int S=node.first,T=node.second;
    G.init();
    int w=G.max_flow(S,T);
    pii tmp1=res1,tmp2=res2;
    Tr.add_edge(S,T,w);
    if(tmp1.second) build(tmp1);
    if(tmp2.second) build(tmp2);
}

最小割的可行边与必须边

可行边:存在于一种最小割方案的边。

必须边:存在于所有最小割方案的边。

首先这需要是满流的边。于是我们在最大流之后的残量网络上跑,如果这条边两端点仍连通,那么这条边不是割。剩下的就是可行边了,而对于必须边而言,其无可替代的地位在于存在一条流,使得这条边是唯一的关键边,要割掉这个流且保证最小割只能割这条,也就是说残量网络上,\(S\)\(u\) 连通,\(v\)\(T\) 连通。

这还不够,直接按照这个判断非常麻烦,我们要借助于最大流算法中用于反悔的反向边。

发现对于判断与源汇是否连通的情况,这条路径一定有流量并且没流满,也就是正反都在残量网络;而判断是不是割,在满流的前提下,反向边出现可以保证,如果这时有正向路径就说明不是割,这其实是求强连通分量。

费用流

EK 费用流

给每条边加上一个费用,即单位流量流经这条边的花费,现在求在保证最大流的前提下的最小花费。

考虑增广的优先级,如果每次增广流量都为 \(1\) 时,一定增广费用最小的一条边,也就是我们以费用最短路为优先级增广是正确的。

反悔时的反向边费用与正向边为相反数,于是求最短路需要用 SPFA,复杂度 \(O(n^2m^2)\),依旧是跑不满。

这里的算法是基于 Edmonds-Karp 的,每次最短路后记录下转移路径,一次只能处理一条增广路。

对于最大费用,在没有负环的情况下可以直接建费用未负的边跑最小费用,为避免负环有时需要拆点。

点击查看代码
int n,m,S,T;
struct Graph{
    struct edge{
        int to,nxt;
        ll lim,c;
    }e[maxm<<1];
    int head[maxn],cnt=1;
    inline void add_edge(int u,int v,ll w,ll c){
        e[++cnt].to=v,e[cnt].nxt=head[u],e[cnt].lim=w,e[cnt].c=c,head[u]=cnt;
        e[++cnt].to=u,e[cnt].nxt=head[v],e[cnt].lim=0,e[cnt].c=-c,head[v]=cnt;
    }
    int pre[maxn];
    ll dis[maxn];
    bool vis[maxn];
    inline void SPFA(){
        queue<int> q;
        memset(dis,0x3f,sizeof(dis));
        memset(vis,0,sizeof(vis));
        dis[S]=0,vis[S]=1;
        q.push(S);
        while(!q.empty()){
            int u=q.front();
            vis[u]=0;
            q.pop();
            for(int i=head[u];i;i=e[i].nxt){
                int v=e[i].to;
                ll c=e[i].c;
                if(dis[u]+c<dis[v]&&e[i].lim){
                    pre[v]=i;
                    dis[v]=dis[u]+c;
                    if(vis[v]) continue;
                    vis[v]=1;
                    q.push(v);
                }
            }
        }
    }
    inline pair<ll,ll> max_flow(){
        ll flow=0,min_cost=0;
        while(1){
            SPFA();
            if(dis[T]==maxxn) return make_pair(flow,min_cost);
            ll mn=maxxn;
            for(int u=T;u!=S;u=e[pre[u]^1].to) mn=min(mn,e[pre[u]].lim);
            flow+=mn;
            for(int u=T;u!=S;u=e[pre[u]^1].to){
                min_cost+=e[pre[u]].c*mn;
                e[pre[u]].lim-=mn,e[pre[u]^1].lim+=mn;
            }
        }
    }
}G;

其实把 Dinic 的 bfs 改成 SPFA 也可以达到同样的效果,不过貌似不是正统做法。

费用流模型构建

大体与最大流相同。

优化建图

动态建图

这样一道题:BZOJ-2879 NOI 2012 美食节

如果直接暴力连边跑的话,点数是 \(O(n+mp)\),边数是 \(O(nmp)\) 的,然而由于流量只有 \(p\),有很多边是无用的,而考虑到同一个厨师,相对更早建出的点一定更优,就需要使用动态建图。

具体是每次增广时,如果经过了某个厨师的最新节点时,说明我们需要下一步可能会增广更新的一个节点,这时再把这个节点建出来,于是点数可以优化到 \(O(n+m+p)\),边数可以优化到 \(O(n(m+p))\)

上下界网络流

给流量一个下界的限制,即满足 \(b(u,v)\le f(u,v)\le c(u,v)\)

无源汇上下界可行流

也就是在流量确定的情况下,可以无限地流。

与正常网络流不同的地方就在于有一个下界,先假定每个边的流量为下界,设 \(W_u=\sum_{(x,u)\in E} b(x,u)-\sum_{(u,x)\in E} b(u,x)\)

建立超级源点 \(SS\),超级汇点 \(TT\),将 \(SS\) 与所有 \(W_u\) 为正的点相连,表示在以下界为流量情况下多流入了 \(|W_u|\),同理将所有 \(W_u\) 为负的点相连,表示在以下界为流量情况下多流出了 \(|W_u|\)。同时把原图的边调整成 \(c(u,v)-b(u,v)\)

这样求出的最大流如果与 \(SS\) 连出边的容量总和相等,说明经过调整可以得到一条可行流。

有源汇上下界可行流

\(T\)\(S\) 连一条 \([0,+\infty]\) 的边,转化成无源汇问题。

而这条边的流量也就是 \(S\to T\) 的流量,即要求的可行流。

有源汇上下界最大流

在可行流的基础上解决问题,此时由于我们已然保证下界,刚刚跑过最大流的图中,属于原图上边的流量加上其对应下界就构成了一种方案,在原图的残量网络上跑最大流即可。

这里有两种正确的写法,一是跑完第一次无源汇之后删去增加的边,答案为这条边的流量与残量网络上最大流的和;二是不删这条边,直接跑最大流,由于可行流答案是 \(T\to S\) 的流量,也就是反向边中 \(S\to T\) 的剩余容量,因此直接跑最大流也可以统计到这个答案。

有源汇上下界最小流

依旧是在可行流的残量网络上处理,保证最小就要退掉一些无用的,于是跑 \(T\to S\) 最大流,用可行流减去即可。

参考资料

posted @ 2022-12-15 11:04  SoyTony  阅读(254)  评论(0编辑  收藏  举报