网络流最大流的基础问题

对于网络流的最大流,实际上求的是从 s − > t s->t s>t的最大流量,也就是输入 s s s,最多有多少能到 t t t
这里引入残量网络的概念,就是容量减去已经使用的流量。(可以理解为一秒钟使用的流量)
做法是
(1)不断去求增广路,增广路就是一条路径 : s − > t :s->t :s>t,但是每条边都是有剩余流量的(走之前)。
(2)每次求出增广路之后,对于每一条边减去路径中最小的边流量。
这样子是不可能求出正解,网络流之所以正确,是因为引入了反向边的概念,也就是我们对于每一条边都有一条反向边,容量为0.
每次你做出选择,减去最小流量的时候,需要加上反向边的流量。也就是说就是这一步是错的,也可以通过反向边消除错误的影响。(貌似博客中的数据结构J题是引用了这种思想)
到这里:网络流最大流的基本思想就已经结束了。


事实上,找出增广路我只要找到一条合法路径即可,所以我们采用 B F S BFS BFS的方式而不是 D F S DFS DFS,BFS层次增加只要到终点即可,往往比一头到底的 d f s dfs dfs合理的多。见lrj老师的EK算法代码,有简单的注释说明:

struct Edge{
    int from,to,cap,flow;
    Edge(int u,int v,int c,int f):from(u),to(v),cap(c),flow(f){}
};

struct EdmondsKarp{
    int tmp;
    vector<Edge>edges;//存正反向边
    vector<int>G[maxn];//邻接表,G[i][j]表示结点i的第j条边在e数组中的序号
    int a[maxn];//当起点到i的可改进量。
    int p[maxn];//最短路树上的入弧编号

    void init(int n){
        for(int i=1;i<=n;i++)G[i].clear();
        edges.clear();
    }

    void AddEdge(int from,int to,int cap){
        edges.push_back(Edge(from,to,cap,0));
        edges.push_back(Edge(to,from,0,0));//反向弧
        tmp=edges.size();
        G[from].push_back(tmp-2);
        G[to].push_back(tmp-1);
        //cout<<"?"<<endl;
    }

    int Maxflow(int s,int t){
        int flow=0;
        for(;;){
            memset(a,0,sizeof(a));
            queue<int>Q;
            Q.push(s);
            a[s]=inf;
            while(!Q.empty()){
                int x=Q.front();Q.pop();
                for(int i=0;i<G[x].size();i++){
                    Edge &e = edges[G[x][i]];
                    if(!a[e.to]&&e.cap>e.flow){
                        p[e.to]=G[x][i];//记录这个点被那条边得到的
                        a[e.to]=min(a[x],e.cap-e.flow);//找到增广路的最小流量
                        Q.push(e.to);
                    }//一个点由于BFS走最短路所以尽可能的小,每个点只走一遍
                }
                if(a[t])break;
            }
            if(!a[t])break;//说明没有找到增广路。
            for(int u=t;u!=s;u=edges[p[u]].from){
                edges[p[u]].flow+=a[t];//流量增大,残余流量减少
                edges[p[u]^1].flow-=a[t];//异或可以对奇数取偶数,对偶数取奇数。(+1或者-1得到)
            }
            flow+=a[t];
        }
        return flow;
    }
}EK;

对于最大流,我们先不谈对EK算法的进一步升级,我们谈论一个问题:关于最大流最小割定理
最小割是与最大流密切相关的一个问题。
最小割是这样的:把所有顶点分成两个集合 S S S V − S V-S VS,其中源点 s s s在集合S中,汇点 t t t在集合 T T T中的边全部删除,就无法从 s s s t t t了。这样的集合划分 ( S , T ) (S,T) (S,T)称为一个 s − t s-t st割,它的容量定义为: c ( S , T ) = ∑ u ∈ S , t ∈ T c ( u , v ) c(S,T)=\sum_{u∈S,t∈T}c(u,v) c(S,T)=uS,tTc(u,v),即起点在S中,终点在T中所有边的容量和。
这里给出简单的证明:
我们可以这样看待割,从 s s s运送到 t t t的物品必然通过跨越 S S S T T T的边,所以从 s s s t t t的净流量等于 ∣ f ∣ = f ( S , T ) = ∑ u ∈ S , v ∈ T f ( u , v ) ≤ ∑ u ∈ S , v ∈ T c ( u , v ) = c ( S , T ) |f|=f(S,T)=\sum_{u∈S,v∈T}f(u,v)≤\sum_{u∈S,v∈T}c(u,v)=c(S,T) f=f(S,T)=uS,vTf(u,v)uS,vTc(u,v)=c(S,T)
因为可能有回来的流,所以会小于。
当残量网络没有增广路的时候,把已经更新的点(指这次找的过程还有残量)作为 S S S,其他的为 T T T,那么 S − &gt; T S-&gt;T S>T的边都是满载的,且不可能有回来的流量,也就是取了等号
值得注意的是:上面的是恒等式,对于任何可行流(合法增广路)和割都是成立的,也就是对最大流和最小割成立。换句话说,取等号的时候同时取得最小割和最大流
证毕。
这里呢其实有一个用到最小割的地方:最大权闭合子图。(谢谢博主)
总体意思就是,有一些点是有价值的,这些点分别对应了一个集合,集合内有一些点是要花费的。求最大价值。
如果我们从源点连接边到价值点,容量为价值;从集合点连接边到汇点,也是花费作为容量。中间的容量都是 i n f inf inf.
那么我们要使得两边不能到达的话,最小割一定在左右两侧(最简单的就是一边所有边的集合就是割)
左边的边如果选择了作为割就是不要这个权值,右边的边选择了就是把负点留了下来,就是要这个花费。我们可以发现,左边没割掉的连过来,一定连不到底(因为连接的负点到底被割掉了),如果没有被割掉那么源点汇点仍然连通
这样是满足题目条件,同时这里的最小割=不要的价值+要的费用。
那么价值总和减去最小割即是答案。这里比较通俗,其实这是一个最大权闭合子图的例子,详细见上面博客。


这里我们再来讨论之前的EK算法进行增广,这样最多需要 O ( n m ) O(nm) O(nm)次增广, B F S BFS BFS的效率进行查找,每次需要 O ( m ) O(m) O(m),总时间复杂度高达 O ( n m 2 ) O(nm^2) O(nm2)详细解析
这样的复杂度是我们不能够接受的,接下来我们要介绍的一种算法称为 D i n i c Dinic Dinic。这种算法就是不停地 B F S BFS BFS造出层次图,然后用阻塞流进行增广
层次图就是:在残量网络中,点 s s s到点 u u u地距离为3(边地长度为1),那么 u u u在第三层。
阻塞流:不考虑反向弧时地“极大流”。[我是不懂这句话的,但是换句话说就是:从起点在层次图里 d f s dfs dfs,每找到一条路就增广。(多路增广)]。多路增广的结果就是无法继续增广。
下一次重建层次图,多了反向边所以会有新的层次,会有新的增广路,直到无法达到汇点。
这个算法的时间复杂度为 O ( n 2 m ) O(n^2m) O(n2m),阻塞流计算是 O ( n m ) O(nm) O(nm),最多计算 n − 1 n-1 n1次阻塞流,可以证明每次多路增广后,层次标号一定会变化,相较于之前一定会存在回溯再往前,往前的层次一定和原来相同,不可能断层,否则就过不去了。所以总共走的路一定比之前长,而最长只可能是n。对于 d f s dfs dfs,每增广一次最多加上回退也就经过n个点,一共最多有m次增广(因为每次都会删除一条边,不会存在删了又加加了又删,因为还要满足层次关系),因为每次增广都会删去一条边。事实上算法远远到不了这个上界。**


主要思路:分层图,在图中跑一条多分叉增广路(尽可能地,所以我们选择一条边a,接下来如果可以在a的容量内多加上几条边得容量,这样子会更省时间。直到找不到增广路)
具体复杂度分析
l r j lrj lrj大神的模板代码:用递归实现,这里我稍微整理了下,加了点注释。

struct Edge{
    int from,to;
    ll cap,flow;
};

struct Dinic{
    int n,tmp,s,t;
    vector<Edge>edges;
    vector<int>G[maxn];//邻接表用,存储的是边在edges中的序号
    bool vis[maxn];//BFS使用
    int d[maxn];//从起点到i的距离
    int cur[maxn];//当前弧下标

    void init(int n,int s,int t){
        this->n=n,this->s=s,this->t=t;
        edges.clear();
        for(int i=1;i<=n;i++)G[i].clear();
    }

    void AddEdge(int from,int to,ll cap){
        edges.push_back((Edge){from,to,cap,0});
        edges.push_back((Edge){to,from,0,0});
        tmp=edges.size();
        G[from].push_back(tmp-2);
        G[to].push_back(tmp-1);
    }

    bool BFS(){
        memset(vis,0,sizeof(vis));
        queue<int>q;
        q.push(s);
        d[s]=0,vis[s]=1;
        while(!q.empty()){
            int x=q.front();q.pop();
            for(int i=0;i<G[x].size();i++){
                Edge& e = edges[G[x][i]];
                if(!vis[e.to]&&e.cap>e.flow){
                    vis[e.to]=1;
                    d[e.to]=d[x]+1;
                    q.push(e.to);
                }//只考虑残量网络中的弧
            }
        }
        return vis[t];//用于判断是否能走到底。
    }

    int DFS(int x,ll a){//多路增广
        if(x==t||a==0)return a;//a表示的是当前最小,也就是接下来能用的不能超过a
        ll flow=0,f;
        for(int& i=cur[x];i<G[x].size();i++){//能保证一个dfs中不重复走同样的边(对于同一个节点),因为走过的边一定是满载的了。
            Edge& e = edges[G[x][i]];
            if(d[x]+1==d[e.to]&&(f=DFS(e.to,min(a,e.cap-e.flow)))){
                e.flow+=f;
                edges[G[x][i]^1].flow-=f;
                flow+=f;
                a-=f;
                if(a==0)break;//f表示从这个e.to的点开始使用的最大流。
            }
        }
        return flow;
    }

    ll Maxflow(){
        ll flow=0;
        while(BFS()){
            memset(cur,0,sizeof(cur));
            flow+=DFS(s,inf);
        }
        return flow;
    }
}dc;

d i n i c dinic dinic算法还是不够快的,如果出了爆栈的情况,我们应该使用下面的算法( d i n i c dinic dinic也可以使用非递归的写法,但是没必要,可以使用一种叫 I S P A ISPA ISPA的算法)。
我们可以把 d f s dfs dfs后每次都要进行的 b f s bfs bfs继续简化:
基于这样的一个事实,任意节点到汇点的最短距离(也就是层数)都不会减小[用完了之后只会有回溯从而层数增加],所以最短距离会不断增加。我们只要在增广过程不断修改这个下界,相当于 d i n i c dinic dinic中的 b f s bfs bfs更新层数的操作。
增广的过程中类似于 d i n i c dinic dinic只允许 d [ i ] = d [ j ] + 1 d[i]=d[j]+1 d[i]=d[j]+1的弧走,这里的 d d d换成了到汇点的距离。
如果走不动的话,对于之前的 d i n i c dinic dinic重新构造分层图,但是这里我们只能当场修改,具体来说,对于 i − &gt; j i-&gt;j i>j,计算残量网络中的边来更新出 d [ i ] = m i n ( d [ j ] ) + 1 d[i]=min(d[j])+1 d[i]=min(d[j])+1,层数应当是增加了的。
如果没有从 i i i出发的弧,则设 d ( i ) = n − 1 d(i)=n-1 d(i)=n1,因为不可能会有断层的(之后会有判断,所以先设置为最高层,因为没有直接相连的话,说明已经到了最远处了,所以没办法考虑了。(目前先这样理解)
每次更新的时候需要回溯之前的点,因为都需要修改。
接下来是两个小优化,第一个是弧优化,边不应该重复使用,用 c u r cur cur数组记录。
第二个是 g a p gap gap优化,记录距离层的点数量,类似于之前的莫队求众数。一旦出现0的层数,断层也就意味着完全找不到增广路,算法可以直接结束。
另外一个种终止条件就是 d [ s ] = n d[s]=n d[s]=n距离层数达到最大。(应该是这么理解)
P S : I S P A PS:ISPA PSISPA算法的理解只能先到这里了,上面仅仅是我目前的理解,可能会有些理解偏差,这可能需要以后做题或者重新理解来弥补,时间实在不够了(花了两天

struct Edge{
    int from,to;
    ll cap,flow;
};

struct ISAP{
    int n,tmp,s,t;
    vector<Edge>edges;
    vector<int>G[maxn];
    bool vis[maxn];
    int d[maxn],cur[maxn];
    int p[maxn];//可增广路上的上一条弧
    int num[maxn];

    void init(int n,int s,int t){
        this->n=n,this->s=s,this->t=t;
        edges.clear();
        FOR(i,1,n)G[i].clear();
    }

    void AddEdge(int from,int to,ll cap){
        edges.push_back((Edge){from,to,cap,0});
        edges.push_back((Edge){to,from,0,0});
        tmp=edges.size();
        G[from].push_back(tmp-2);
        G[to].push_back(tmp-1);
    }

    void BFS(){
        memset(vis,0,sizeof(vis));
        d[t]=0;vis[t]=1;
        queue<int>Q;Q.push(t);
        while(!Q.empty()){
            int x=Q.front();Q.pop();
            for(int i=0;i<G[x].size();i++){
                Edge& e=edges[G[x][i]];
                if(!vis[e.to]){
                    vis[e.to]=1;
                    d[e.to]=d[x]+1;
                    Q.push(e.to);
                }
            }
        }
    }

    ll Augment(){
        int x=t;ll a=inf;
        while(x!=s){
            Edge& e=edges[p[x]];
            a=min(a,e.cap-e.flow);
            x=edges[p[x]].from;
        }
        x=t;
        while(x!=s){
            edges[p[x]].flow+=a;
            edges[p[x]^1].flow-=a;
            x=edges[p[x]].from;
        }
        return a;
    }

    ll Maxflow(){
        ll flow=0;
        BFS();
        memset(num,0,sizeof(num));
        for(int i=1;i<=n;i++)num[d[i]]++;
        int x=s;
        memset(cur,0,sizeof(cur));
        while(d[s]<n){
            if(x==t){
                flow+=Augment();
                x=s;
            }
            int ok=0;
            for(int i=cur[x];i<G[x].size();i++){
                Edge& e=edges[G[x][i]];
                if(e.cap>e.flow&&d[x]==d[e.to]+1){
                    ok=1;
                    p[e.to]=G[x][i];
                    cur[x]=i;
                    x=e.to;
                    break;
                }
            }
            if(!ok){
                int m=n-1;
                for(int i=0;i<G[x].size();i++){
                    Edge& e=edges[G[x][i]];
                    if(e.cap>e.flow)m=min(m,d[e.to]);
                }
                if(--num[d[x]]==0)break;
                num[d[x]=m+1]++;
                cur[x]=0;
                if(x!=s)x=edges[p[x]].from;
            }
        }
        return flow;
    }
}ip;

下面对于网络最大流,我们添加一个因素,费用。就是对于每条边,加上单位流量所需费用。
最小费用最大流就是在总流量最大的情况下,求出总费用最小的流,即最小费用流。
最小费用流中平行边是很有意义的,因为会影响价值,如果没有费用完全可以合并(不用考虑也行)。
如果出现负环(那么总价值可以无限小)
最小费用流的算法很简单,只要初始流是该流量下的最小费用可行流,每次增广都是新流量下的最小费用流,最后就能得到正解。
因为存在负权,我们采用 s p f a spfa spfa进行最短路增广。和EK算法核心很像。

struct Edge{
    int from,to;
    ll cap,flow,cost;
};

struct MCMF{
    int n,tmp,s,t;
    vector<Edge>edges;
    vector<int>G[maxn];
    int inq[maxn];
    ll d[maxn];//spfa
    int p[maxn];//上一条弧便于回溯
    ll a[maxn];//最小改进量

    void init(int n,int s,int t){
        this->n=n,this->s=s,this->t=t;
        edges.clear();
        for(int i=1;i<=n;i++)G[i].clear();
    }

    void AddEdge(int from,int to,ll cap,ll cost){
        edges.push_back((Edge){from,to,cap,0,cost});
        edges.push_back((Edge){to,from,0,0,-cost});
        tmp=edges.size();
        G[from].push_back(tmp-2);
        G[to].push_back(tmp-1);
    }

    bool spfa(int s,int t,ll& flow,ll& cost){
        for(int i=0;i<=n;i++)d[i]=inf;
        memset(inq,0,sizeof(inq));
        d[s]=0,inq[s]=1,p[s]=0,a[s]=inf;
        queue<int>Q;
        Q.push(s);
        while(!Q.empty()){
            int u=Q.front();Q.pop();
            inq[u]=0;
            for(int i=0;i<G[u].size();i++){
                Edge& e=edges[G[u][i]];
                if(e.cap>e.flow&&d[e.to]>d[u]+e.cost){
                    d[e.to]=d[u]+e.cost;//松弛
                    p[e.to]=G[u][i];//记录上一条弧
                    a[e.to]=min(a[u],e.cap-e.flow);//最小可改进量
                    if(!inq[e.to]){Q.push(e.to);inq[e.to]=1;}//入队
                }
            }
        }
        if(d[t]==inf)return false;//说明不连通了。
        flow+=a[t];//如果固定流量的话,可以在flow+a>=k的时候只增广到k,然后终止程序
        cost+=d[t]*a[t];
        int u=t;
        while(u!=s){
            edges[p[u]].flow+=a[t];
            edges[p[u]^1].flow-=a[t];
            u=edges[p[u]].from;
        }
        return true;
    }

    ll Mincost(){//绝对不能有负权圈,否则连续最短路的数学证明失效
        ll flow=0,cost=0;
        while(spfa(s,t,flow,cost));
        cout<<flow<<" "<<cost<<endl;
        return flow;
    }
}mf;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值