• 网络流学习笔记


    1 - 最大流

    问题概述

    基本概念参见 OI-Wiki

    有一张图,要求从源点流向汇点的最大流量(可以有很多条路到达汇点)。

    高科技:O(nm)最大流

    Edmond-Karp(EK)

    复杂度:(mathcal{O}(nm^2)) ,处理范围在 (1e3sim 1e4) 左右。

    思路:从源点到汇点有很多条路径,每次抽取一条,并找出路径上的边权最小值 (x) ,然后把路径上所有边权都减去(x)(也就是找增广路,这条路径上流过 (x) 的流量),直到没有路径可以给汇点增加流量为止。显然, (0) 容量是没有用的。具体实现采用 BFS 。

    如果是单纯的建边跑不了最大流,因为如果之前选的并不是最优路径后面就不一定能取到最大值。EK 支持一个 反悔操作 ,也就是建反向边,初始为 (0) ,如果对应的正向边剩余容量减少,那么相应的反向边容量就增加,之后反向边就能流动了。

    //Author:RingweEH
    //P3376 【模板】网络最大流
    const int N=210,M=5e3+10;
    const ll INF=0x7f7f7f7f;
    struct Edge
    {
    	int to,nxt; ll val;
    }e[M<<1];
    int n,m,S,T,tot=1,head[N],pre[N],mp[N][N];
    ll mxflow,dis[N];
    bool vis[N];
    
    void Add( int u,int v,int ll w )
    {
    	e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; e[tot].val=w;
    	e[++tot].to=u; e[tot].nxt=head[v]; head[v]=tot; e[tot].val=0;	//反向边
    }
    
    bool BFS()
    {
    	for ( int i=1; i<=n; i++ )
    		vis[i]=0;
    	queue<int> q; q.push( S ); vis[S]=1; dis[S]=INF;
    	while ( !q.empty() )
    	{
    		int u=q.front(); q.pop();
    		for ( int i=head[u]; i; i=e[i].nxt )
    		{
    			if ( e[i].val==0 ) continue;	//流没了
    			int v=e[i].to;
    			if ( vis[v] ) continue; 
    			dis[v]=min( dis[u],e[i].val );	//取路径上的最小剩余容量
    			pre[v]=i; 		//记录从哪条边过来
    			q.push( v ); vis[v]=1;
    			if ( v==T ) return 1;	//走到汇点了
    		}
    	}
    	return 0;
    }
    
    void Update()		//每次找到一条增广路,并更新路径上的值
    {
    	int u=T;
    	while ( u^S )
    	{
    		int v=pre[u];
    		e[v].val-=dis[T]; e[v^1].val+=dis[T];		
    		//这条增广路的流量,用来更新路径上所有边和反向边
    		u=e[v^1].to;		//本来记录的是过来的边,回去要反向
    	}
    	mxflow+=dis[T];		//总流量增加
    }
    
    int main()
    {
    	n=read(); m=read(); S=read(); T=read();
    	for ( int i=1; i<=m; i++ )
    	{
    		int u,v; ll w; u=read(); v=read(); w=read();
    		if ( mp[u][v]==0 ) Add( u,v,w ),mp[u][v]=tot;
    		else e[mp[u][v]-1].val+=w;
    		//重边容量合并
    	}
    
    	while ( BFS() ) Update();
    
    	printf( "%lld
    ",mxflow ); 
    
    	return 0;
    }
    

    Dinic

    复杂度 (mathcal{O}(nm^2)) ,处理范围在 (1e4sim 1e5) 左右,稠密图较明显,求解二分图最大匹配是 (mathcal{O}(msqrt n)) .

    思路:BFS 得到分层图,(d[x]) 表示层次(即 (S)(x) 的最少边数)。残量网络中,满足 (d[y]=d[x]+1) 的边 ((x,y)) 构成的子图称为分层图。显然,这一定是一张有向无环图,从 (S) 开始 DFS ,每次向下找一个点,直到到达 (T) ,然后回溯回去,找另外的点搜索出多条增广路。

    总结:

    • 在残量网络上 BFS 求出节点层次,构造分层图
    • 在分层图上 DFS 找增广路,回溯的同时更新边权

    当前弧优化 :(没加这个复杂度是假的)

    对于一个节点 (u) ,在 DFS 中, for ( int i=head[u]; i; i=e[i].nxt ) 走到了第 (i) 条弧时,前 (i-1) 条到汇点 (T) 的路径一定流满了,再访问 (u) 时前 (i-1) 条就没有意义了。所以每次枚举 (u) 的时候,要改变枚举的起点,即 (nw)for ( int i=now[x]; i; i=e[i].nxt ) .

    //Author:RingweEH
    //P3376 【模板】网络最大流
    const int N=210,M=5e3+10;
    const ll INF=0x7f7f7f7f;
    struct Edge
    {
    	int to,nxt; ll val;
    }e[M<<1];
    int n,m,S,T,tot=1,nw[N],head[N];
    ll mxflow,dis[N];
    
    void Add( int u,int v,ll w )
    {
    	e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; e[tot].val=w;
    	e[++tot].to=u; e[tot].nxt=head[v]; head[v]=tot; e[tot].val=0;
    }
    
    bool BFS()		//构建分层图,判断残量网络还能不能到达汇点
    {
    	for ( int i=1; i<=n; i++ )
    		dis[i]=INF;
    	queue<int> q; q.push( S ); dis[S]=0; nw[S]=head[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].val>0 && dis[v]==INF )
    			{
    				q.push( v ); nw[v]=head[v]; dis[v]=dis[u]+1;
    				if ( v==T ) return 1;
    			}
    		}
    	}
    	return 0;
    }
    
    int DFS( int u,ll sum )		//sum记录增广路上的最小容量
    {
    	if ( u==T ) return sum;
    	ll res=0;
    	for ( int i=nw[u]; i && sum; i=e[i].nxt )
    	{
    		nw[u]=i; int v=e[i].to;
    		if ( e[i].val>0 && (dis[v]==dis[u]+1) )
    		{
    			ll tmp=DFS( v,min(sum,e[i].val) );
    			if ( tmp==0 ) dis[v]=INF;	//v不能再增广了,剪枝
    			e[i].val-=tmp; e[i^1].val+=tmp;
    			res+=tmp; sum-=tmp;		//res是经过该点的流量和,sum是经过该点剩余流量
    		}
    	}
    	return res;
    }
    
    int main()
    {
    	n=read(); m=read(); S=read(); T=read();
    	for ( int i=1; i<=m; i++ )
    	{
    		int u=read(),v=read(); ll w=read();
    		Add( u,v,w );
    	}
    
    	while ( BFS() ) mxflow+=DFS(S,INF);
    
    	printf( "%lld
    ",mxflow );
    
    	return 0;
    }
    

    ISAP

    复杂度:(mathcal{O}(n^2m))

    看看 Dinic 的 while ( BFS() ) mxflow+=DFS(S,INF); ,这样重复 BFS 显然效率低下。于是出现了 ISAP ,只需要跑一遍 (bfs)

    大体流程:

    • (T)(S) 跑 BFS
    • (S)(T) 跑 DFS
    • 重复第二步的 DFS 直到出现断层

    ISAP 只跑一遍 BFS 标记层次,然后每个点的层次随着 DFS 变高。记 (gap[i]) 表示高度为 (i) 的点的个数,(d[i]) 表示 (i) 的层次,当结束 (i) 的增广之后,遍历残量网络上 (i) 的所有出边,找到 (d) 最小的出点 (j) ,令 (d[i]=d[j]+1) ,如果没有出边,(d[i]=n) 。当 (gap[i]=0) 时出现断层,(S)(T) 不连通,就可以停止了(这其实是 GAP 优化)。

    和 Dinic 类似,ISAP 同样有当前弧优化。且最短路的修改具有连续性,每次不需要真的遍历残量网络出边,直接给 (d[i]) 加一即可。

    //Author:RingweEH
    const int N=210,M=5e3+10;
    const ll INF=0x7f7f7f7f;
    struct Edge
    {
        int to,nxt; ll val;
    }e[M<<1];
    int n,m,S,T,tot=1,head[N],dep[N],gap[N],nw[N];
    ll mxflow=0;
    
    void Add( int u,int v,ll w )
    {
        e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; e[tot].val=w;
        e[++tot].to=u; e[tot].nxt=head[v]; head[v]=tot; e[tot].val=0;
    }
    
    void BFS()
    {
        memset( dep,-1,sizeof(dep) ); memset( gap,0,sizeof(gap) );
        dep[T]=0; gap[0]=1;
        queue<int> q; q.push( T );
        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 ( dep[v]>-1 ) continue;
                q.push( v ); dep[v]=dep[u]+1; gap[dep[v]]++;
            }
        }
    }
    
    int DFS( int u,ll sum )
    {
        if ( u==T ) { mxflow+=sum; return sum; }
        ll res=0;
        for ( int i=nw[u]; i; i=e[i].nxt )
        {
            nw[u]=i; int v=e[i].to;
            if ( e[i].val && dep[v]+1==dep[u] )
            {
                int tmp=DFS( v,min(e[i].val,sum) );
                if ( tmp )
                {
                    e[i].val-=tmp; e[i^1].val+=tmp;
                    sum-=tmp; res+=tmp;
                }
                if ( sum==0 ) return res;
            }
        }
        gap[dep[u]]--; 
        if ( gap[dep[u]]==0 ) dep[S]=n+1;
        dep[u]++; gap[dep[u]]++;
        return res;
    }
    
    int main()
    {
        n=read(); m=read(); S=read(); T=read();
        for ( int i=1; i<=m; i++ )
        {
            int u=read(),v=read(); ll w=read();
            Add( u,v,w );
        }
    
        BFS(); mxflow=0;
        while ( dep[S]<n ) 
        {
            memcpy( nw,head,sizeof(head) );  DFS( S,INF );
        }
    
        printf( "%lld
    ",mxflow );
    
        return 0;
    }
    

    HLPP(最高标号预流推进)

    推荐博客

    前面的三种算法都是基于 增广路 的思想。

    而这里是另一类:预流推进

    预流推进思想:允许水流在非源汇的节点中暂时保存(称为 超额流 ),同时伺机将超额流 推送 出去。只要最后所有的节点超额流为 (0) ,那么就是合法的。为了避免死循环,引入每个节点的 高度 ,只允许高度较高的节点向高度较低的推送。如果一个节点因为高度原因不能推送出去,那么就抬高这个高度,这个操作称为 重贴标签

    HLPP算法流程:用 (e[v]) 表示超额流,(h[v]) 表示高度。

    首先,将所有除了源点以外的节点高度设为 (0) ,源点 (S) 设为 (n) ,并将 (S) 的所有边都充满流量推送出去(放水)。将所有推送后超额流不为 (0) 的节点加入以高度为关键字的优先队列中等待推送。

    然后,每次取出对首 (u) 并尝试推送:

    • 逐一检查 (u) 的出边,如果某条边还有流量,并且另一端 (v) 满足 (h[v]+1=h[u]) ,那么这条边可以推送。
    • 推送流量不能超过边的容量,也不能超过 (e[u])
    • 推送完毕后修改超额流,如果 (v) 不在队列中那么加入

    完成之后,如果 (e[u]>0) ,那么说明需要对 (u) 重贴标签。找到有流量并且另一端 (h[v]) 最小的边,令 (h[u]=h[v]+1) ,把操作完的 (u) 加入优先队列。

    直到队列为空。

    虽然这样复杂度正确,为 (mathcal{O}(n^2sqrt m)) ,但是需要一些优化才能在随机数据下和增广路算法相当。

    1. 可以通过一边 BFS 将每个点的初始高度设为到汇点的距离。(除了 (S)
    2. 如果一个点被重贴标签之后,原来的高度已经没有点了,那么高于它的点必然不能推到 (T) ,所以可以将高度大于 (h[u]) 并且小于 (n+1)(h) 设为 (n+1) ,以便尽快推给 (S) 。也就是和 ISAP 一样的 GAP 优化。

    注意 gap 大小要开两倍。


    然而事实上博主的写法并不适合我这种大常数选手。于是跑到 LOJ 上去学习了一番最优解,得到了如下写法。

    这写法长这样:

    记录高度为 (i) 的节点个数,具体节点,高度为 (i) 的有超额流的节点,记录更改高度的总次数,记录最大高度;

    每次重贴标签直接从 (T) 开始重新对着残量网络 BFS;

    一边往外流一边记录所有出边的端点高度最小值(为了有剩余超额流的时候直接更新高度);

    断层就直接把所有高度比当前高的节点置为 INF ;

    初始化源点无限流量,先重贴一遍,然后按照高度从高到低依次往外推流,如果更改高度的总次数大于某个阀值,那么就再重贴一遍。

    具体可以看代码QWQ 跑得飞快。

    //Author:RingweEH
    #define pb push_back
    const int N=1203,INF=0x3f3f3f3f;
    struct Edge
    {
        int to,nxt,val;
        Edge ( int _to,int _nxt,int _val ) : to(_to),nxt(_nxt),val(_val) {}
    };
    vector<Edge> g[N];
    int n,m,S,T,h[N],cnt[N],work,ht,rest[N];
    vector<int> las[N],gap[N];
    
    void Add( int u,int v,int w )
    {
        g[u].pb( Edge(v,g[v].size(),w) );
        g[v].pb( Edge(u,g[u].size()-1,0) );
    }
    
    void Update_height( int u,int nw )
    {
        ++work;         //work记录更改高度的次数
        if ( h[u]^INF ) --cnt[h[u]];
        h[u]=nw; 
        if ( nw==INF ) return;
        ++cnt[nw]; ht=nw;    //cnt[i]记录高度为i的节点个数
        gap[nw].pb(u);          //gap[i]记录高度为i的节点
        if ( rest[u] ) las[nw].pb(u);   //las[i]记录高度为i且还有超额流的节点
    }
    
    void Relabel()      //重置高度
    {
        work=0; memset( h,0x3f,sizeof(h) ); memset( cnt,0,sizeof(cnt) );
        for ( int i=0; i<=ht; i++ )
            las[i].clear(),gap[i].clear();
        h[T]=0; queue<int> q; q.push(T);
        while ( !q.empty() )        //每次重新BFS
        {
            int u=q.front(); q.pop();
            for ( Edge &e : g[u] )
                if ( h[e.to]==INF && g[e.to][e.nxt].val ) //如果还有流量且另一个端点没被更新过
                {
                    q.push( e.to ); Update_height( e.to,h[u]+1 );   //设为到汇点的距离
                }
            ht=h[u];        //记录最大的一个高度
        }
    }
    
    void Push( int u,Edge &e )      //u的超额流通过边e往外流
    {
        if ( rest[e.to]==0 ) las[h[e.to]].pb(e.to);     //如果没在里面,现在就要在了
        int del=min( rest[u],e.val );   
        e.val-=del; g[e.to][e.nxt].val+=del;
        rest[u]-=del; rest[e.to]+=del;
    }
    
    void Push_Flow( int u )
    {
        int nh=INF;
        for ( Edge &e : g[u] )   
            if ( e.val )
                if ( h[u]==h[e.to]+1 )
                {
                    Push( u,e );
                    if ( rest[u]<=0 ) return;
                }
                else nh=min( nh,h[e.to]+1 );        
                //一边往外流一边以防万一要更新,收集最小值
        if ( cnt[h[u]]>1 ) Update_height( u,nh );       //没断层,直接更新
        else
            for ( int i=h[u]; i<N; i++ )        //断层了
            {
                for ( int j : gap[i] ) 
                    Update_height( j,INF );
                gap[i].clear();
            }
    }
    
    int HLPP()
    {
        memset( rest,0,sizeof(rest) );
        rest[S]=2147483647;
        Relabel();
        for ( Edge &e : g[S] )      //从源点往外推
            Push( S,e );
        for ( ; ~ht; --ht )
            while ( !las[ht].empty() )      //枚举高度,从高到低依次往外推
            {
                int u=las[ht].back(); las[ht].pop_back();
                Push_Flow( u );
                if ( work>20000 ) Relabel();        //如果改变得过多了那么就重新标记
            }
        return rest[T];
    }
    
    int main()
    {
        n=read(); m=read(); S=read(); T=read();
        for ( int i=1,u,v,w; i<=m; i++ )
            u=read(),v=read(),w=read(),Add( u,v,w );
        printf( "%d
    ",HLPP() );
    }
    

    2 - 最小割

    可以参考这篇论文

    概念&定理

    对于一个网络流图 (G=(V,E)) ,将所有点划分成 (S)(T=V-S) ,其中源点 (sin S) ,汇点 (tin T) . 定义割 ((S,T)) 的容量为所有 (S)(T) 的边的容量之和。

    最大流最小割定理 :最大流=最小割,即 (f(s,t)_{max}=c(S,T)_min)证明

    求方案:可以从源点 (s) 开始 DFS ,每次走残量大于 (0) 的边,找到所有 (S) 点集内的点。

    求割边数量:将容量变为 (1) ,跑 Dinic 即可。

    模型

    有两个集合 (A,B)(n) 个物品,第 (i) 个物品放入 (A) 产生花费 (a_i) ,放入 (B)(b_i) ,还有若干个限制 ((u_i,v_i,w_i)) ,如果 (u_i)(v_i) 不在同一个集合里花费 (w_i) ,每个物品只能属于一个集合,求最小代价。

    二者选其一 的最小割。对每个集合设置源点 (s) 和汇点 (t) ,第 (i) 个点由 (s) 连一条容量为 (a_i) 的边,向 (t) 连一条容量为 (b_i) 的边,在 (u_i,v_i) 之间连容量为 (w_i) 的双向边。当源汇不相连的时候,代表这些点都选择了其中一个集合。如果将连向 (s) 或者 (t) 的边割开,表示不放在 (A) 或者 (B) ,如果物品间割开,表示不放在同一个集合。最小割就是最小花费。

    3 - 费用流

    每条边除了容量限制还有单位流量的费用 (w(u,v)) 。当 ((u,v)) 的流量为 (f(u,v)) 时,花费 (f(u,v) imes w(u,v)) .

    研究问题:最小费用最大流

    SSP

    SSP(Successive Shortest Path)算法:在最大流的 EK 算法求解最大流的基础上,把 用 BFS 求解任意增广路 改为 用 SPFA 求解单位费用之和最小的增广路 即可。

    相当于 把费用作为边权,在残存网络上求最短路

    //Author:RingweEH
    const int N=410,M=15010,INF=0x3f3f3f3f;
    struct Edge
    {
    	int to,nxt,flow,cap;
    	Edge ( int _to=0,int _nxt=0,int _flow=0,int _cap=0 ) :
    	 	to(_to),nxt(_nxt),flow(_flow),cap(_cap) {}
    }e[M<<1];
    int n,m,S,T,tot=1,head[N];
    int pre[N],dis[N],mxflow,mncost;
    bool vis[N];
    
    void Add( int u,int v,int fl,int cap )
    {
    	e[++tot]=Edge(v,head[u],fl,cap); head[u]=tot;
    	e[++tot]=Edge(u,head[v],0,-cap); head[v]=tot;
    }
    
    bool BFS()
    {
    	for ( int i=1; i<=n; i++ )
    		vis[i]=0,dis[i]=INF;
    	queue<int> q; q.push( S ); dis[S]=0;
    	while ( !q.empty() )
    	{
    		int u=q.front(); q.pop(); vis[u]=0;
    		for ( int i=head[u]; i; i=e[i].nxt )
    		{
    			int v=e[i].to; 
    			if ( dis[v]>dis[u]+e[i].cap && e[i].flow )
    			{
    				dis[v]=dis[u]+e[i].cap; pre[v]=i;
    				if ( !vis[v] ) vis[v]=1,q.push( v );
    			}
    		}
    	}
    	return dis[T]!=INF;
    }
    
    void Update()
    {
    	int mn=INF;
    	for ( int i=T; i^S; i=e[pre[i]^1].to )
    		bmin( mn,e[pre[i]].flow );
    	for ( int i=T; i^S; i=e[pre[i]^1].to )
    	{
    		e[pre[i]].flow-=mn; e[pre[i]^1].flow+=mn;
    		mncost+=mn*e[pre[i]].cap;
    	}
    	mxflow+=mn;
    }
    
    int main()
    {
    	n=read(); m=read(); S=1; T=n;
    	for ( int i=1,u,v,f,c; i<=m; i++ )
    		u=read(),v=read(),f=read(),c=read(),Add( u,v,f,c );
    
    	while ( BFS() ) Update();
    
    	printf( "%d %d
    ",mxflow,mncost );
    }
    

    类 Dinic

    在 Dinic 算法的基础上进行改进,把 BFS 求分层图 改为用 SPFA求一条单位费用之和最小的路径(由于有负权的反向边,不能直接用 Dijkstra ),相当于 (w(u,v)) 当做边权在残量网络上求最短路

    优化:使用 Primal-Dual 原始对偶算法,将 SPFA 改成 Dijkstra (有需要看 这篇 ,不过 代码也不长?

    复杂度上界:(mathcal{O}(nmf))

    //Author:RingweEH
    const int N=410,M=15010,INF=0x3f3f3f3f;
    struct Edge
    {
    	int to,nxt,flow,cap;
    	Edge ( int _to=0,int _nxt=0,int _flow=0,int _cap=0 ) :
    	 	to(_to),nxt(_nxt),flow(_flow),cap(_cap) {}
    }e[M<<1];
    int n,m,S,T,tot=1,head[N],dis[N],mxflow,mncost,nw[N];
    bool vis[N];
     
    void Add( int u,int v,int fl,int cap )
    {
    	e[++tot]=Edge(v,head[u],fl,cap); head[u]=tot;
    	e[++tot]=Edge(u,head[v],0,-cap); head[v]=tot;
    }
     
    bool BFS()	
    {
    	for ( int i=1; i<=n; i++ )
    		dis[i]=INF,vis[i]=0;
    	queue<int> q; q.push( S ); dis[S]=0; nw[S]=head[S]; vis[S]=1;
    	while ( !q.empty() )
    	{
    		int u=q.front(); q.pop(); vis[u]=0;
    		for ( int i=head[u]; i; i=e[i].nxt )
    		{
    			int v=e[i].to;
    			if ( e[i].flow && dis[v]>dis[u]+e[i].cap )
    			{
    				nw[v]=head[v]; dis[v]=dis[u]+e[i].cap;
    				if ( !vis[v] ) vis[v]=1,q.push(v);
    			}
    		}
    	}
    	return dis[T]!=INF;
    }
     
    int DFS( int u,int sum )		
    {
    	if ( u==T ) return sum;
    	vis[u]=1; int res=0;
    	for ( int i=nw[u]; i && sum; i=e[i].nxt )
    	{
    		nw[u]=i; int v=e[i].to;
    		if ( !vis[v] && e[i].flow && (dis[v]==dis[u]+e[i].cap) )
    		{
    			int tmp=DFS( v,min(sum,e[i].flow) );
    			if ( tmp )
    			{
    				e[i].flow-=tmp; e[i^1].flow+=tmp;
    				res+=tmp; sum-=tmp;	mncost+=e[i].cap*tmp;
    			}
    		}
    	}
    	vis[u]=0; return res;
    }
     
    int main()
    {
    	n=read(); m=read(); S=1; T=n;
    	for ( int i=1,u,v,f,c; i<=m; i++ )
    		u=read(),v=read(),f=read(),c=read(),Add(u,v,f,c);
     
    	while ( BFS() ) mxflow+=DFS(S,INF);
     
    	printf( "%d %d
    ",mxflow,mncost );
     
    	return 0;
    }
    

    4 - 上下界网络流

    解决每条边流量有上下界限制的网络流。

    无源汇上下界可行流

    问题 :有 (n) 个点, (m) 条边,每条边有一个流量下界 (low) 和流量上界 (up) ,求可行方案使得所有点满足流量平衡的前提下,所有边满足流量限制。

    思路 :两个限制同时考虑会很麻烦,先考虑下界。

    先给每条弧加一个 (low) 的流量下界,那么这个限制就没了。但是现在这个图不一定满足流量平衡,称为初始流

    应该在这张图上再添加一个附加流,使得初始流和附加流之和满足流量平衡。显然,每条弧的附加流上界是 (up-low) ,所以不妨在建边的时候直接设容量为 (up-low) ,提出 (low) . 设现在图中点的流入流出量之差为 (c[i])

    设置虚拟源汇,源点向每个 (c[i]>0) 的点连边,容量为 (c[i]) ;每个 (c[i]<0) 的点向汇点连边,容量为 (-c[i]) . 如果某个点 (x)(c[x]>0) ,那么就从源点给它 (c[x]) 的流,这些流在原本的边上流动,直到流到某个 (c[y]<0) 的点 (y) ,通过这里流出。之后留在原有边上的流量就是附加流。

    如果附加流加上初始流之后流量守恒,那么直接与虚拟源点和汇点相连的边应该都满流。由于 (c[]) 之和为 (0) ,所以新图中的最大流要等于 (sum [c[i]>0]c[i]) . 如果成立那么就存在可行流。求流量就直接找每条边的反向弧即可。

    模板

    //Author:RingweEH
    int main()
    {
        n=read(); m=read(); S=n+1; T=S+1;
        for ( int i=1; i<=m; i++ )
        {
            int u=read(),v=read(),lower=read(),upper=read();
            Add( u,v,upper-lower ); c[v]+=lower; c[u]-=lower;
            low[i]=lower;
        }
    
        int Sum=0;
        for ( int i=1; i<=n; i++ )
        {
            if ( c[i]>0 ) Add( S,i,c[i] ),Sum+=c[i];
            if ( c[i]<0 ) Add( i,T,-c[i] );
        }
     
        while ( BFS() ) mxflow+=DFS(S,INF);
    
        if ( mxflow==Sum )
        {
            printf( "YES
    " );
            for ( int i=3,j=1; j<=m; i+=2,j++ )
                printf( "%lld
    ",e[i].flow+low[j] );
        }
        else printf( "NO
    " );
     
        return 0;
    }
    

    有源汇上下界可行流

    源点和汇点流量不守恒,但是可以手动守恒:从 (T)(S) 连一条下界为 (0) ,上界为 INF 的边,跑无源汇即可。流量就是 (T)(S) 的反向弧流量。

    有源汇上下界最大流

    在有源汇上下界可行流的基础上,在残量网络上跑最大流,相加即可。显然这样不会超出上下界。

    具体实现:用虚拟源汇跑完之后,再用实际的源汇跑跑一遍最大流即可。

    原理:原图中跑完第一次,剩下的就是残量网络,且可行流保存在 (T)(S) 的反向边中。这时候跑 (S)(T) 的最大流,就得到了残量网络最大流+将 (ST) 反向边退回去的流量,也就是最大流加上可行流。

    模板

    //Author:RingweEH
    int main()
    {
        n=read(); m=read(); int s=read(),t=read(); S=n+1,T=S+1;
        for ( int i=1; i<=m; i++ )
        {
            int u=read(),v=read(),lower=read(),upper=read(); low[i]=lower;
            Add( u,v,upper-lower ); c[v]+=lower; c[u]-=lower;
        }
    
        int Sum=0; Add( t,s,INF );
        for ( int i=1; i<=n; i++ )
        {
            if ( c[i]>0 ) Add( S,i,c[i] ),Sum+=c[i];
            if ( c[i]<0 ) Add( i,T,-c[i] );
        }
        while ( BFS() ) mxflow+=DFS(S,INF);
        if ( mxflow!=Sum ) { printf( "please go home to sleep" ); return 0; }
        S=s; T=t; mxflow=0;
        while ( BFS() ) mxflow+=DFS(S,INF);
        printf( "%d
    ",mxflow );
     
        return 0;
    }
    

    有源汇上下界最小流

    和最大流一样,先求出可行流,然后考虑在此基础上减去最大的流量,使之仍然守恒。

    如果我们找到一条 (T o S) 的增广路,那么就说明去掉这条增广路上的流量,仍然守恒。那么就直接从 (T)(S) 跑一遍最大流,用可行流减去流量即可。注意这里 不能 保留 (T)(S) 的 INF 边。

    模板

    //Author:RingweEH
    int main()
    {
        n=read(); m=read(); int s=read(),t=read(); S=n+1,T=S+1;
        for ( int i=1; i<=m; i++ )
        {
            int u=read(),v=read(); ll lower=read(),upper=read(); 
            low[i]=lower; Add( u,v,upper-lower ); c[v]+=lower; c[u]-=lower;
        }
    
        int Sum=0;
        for ( int i=1; i<=n; i++ )
        {
            if ( c[i]>0 ) Add( S,i,c[i] ),Sum+=c[i];
            if ( c[i]<0 ) Add( i,T,-c[i] );
        }
        Add( t,s,INF );
        while ( BFS() ) mxflow+=DFS(S,INF);
        if ( mxflow!=Sum ) { printf( "please go home to sleep" ); return 0; }
        mxflow=e[tot].flow;
        S=t; T=s; e[tot].flow=e[tot^1].flow=0;
        while ( BFS() ) mxflow-=DFS(S,INF);
        printf( "%lld
    ",mxflow );
    
        return 0;
    }
    
  • 相关阅读:
    学好VC++的十大良好习惯
    VC6.0调试技巧
    匈牙利命名法
    VC中常用文件操作(三)
    VL_FEAT——图像处理函数库
    MATLAB——linspace
    (Your)((Term)((Project)))(字符串处理)
    Matrix Chain Multiplication(栈的简单应用)
    The Triangle(DP基础)
    MATLAB——polyfit
  • 原文地址:https://www.cnblogs.com/UntitledCpp/p/Network_Flow.html
Copyright © 2020-2023  润新知