网络流简记

更新日志 2024/12/31:开工。添加网络流概念以及EK算法

2025/01/01:添加Dinic算法,最小割与费用流

2025/01/02:添加最小割方案求法

2025/01/02:添加上下界网络流问题

2026/01/03:更新 Dinic 板子码风。


概念

官方定义

OI-wiki

网络

一种特殊有向图,有一个源点 \(s\) 与汇点 \(t\)。

图中每一条边都具有容量 \(c\),也就是流经流量上限。不存在的边 \(c=0\)。

可以视作流水,从源点开始进水(无限或有限),通过一条条边流开,每条边的尺寸限定了流量。

要求所有流都要汇入汇点。也就是流量守恒。

流

一个整体,大概就是所有有流水的边。

割

两个点集合 \(S,T\),满足 \(S\cup T=V\) 且 \(S\cap T=\varnothing\),同时 \(s\in S,t\in T\)。

一个割的容量是 \(\sum\limits_{u\in S}\sum\limits_{v\in T}c(u,v)\)。

形象化的,就是将点集分成两个集合,割的容量就是连接两个集合的边权之和。

最大流

问题

求一个网络的最大流量。

也就是每条边流量之和。

Ford-Fulkerson 增广

贪心思想。

每次找到一条从 \(s\) 到 \(t\) 的路径,称之为增广路,给它加上 \(w\),也就是增广操作,那么流量就加上了 \(w\)。

为了保证最优,我们加入反悔操作。具体的,每一条边都建立一条反边,反边空闲的流量就是正边已走的流量,这样走反边就相当于反悔了。

正确性证明略。

EK算法

直接模拟 Ford-Fulkerson 算法。

具体的,每次找一条 \(s-t\) 最短路(不是权值和最小,单纯的经过路程最小),保证每一条可行路径均被取到,然后每找到一条路径就操作一次:

  • 给路径中每一条边的流量加上可行的最大值(路径中最小的剩余流量),同时给答案也加上。
  • 别忘了给反边相应减去对应值。

具体地,BFS 即可。证明略。

复杂度 \(O(nm^2)\)。

模板

struct EK{
    int n,cnt;
    int hd[N];
    struct edge{
        int ne,to;
        ll cap,flow;
    }ed[M*2];
    int p[N];ll a[N];
    queue<int> q;

    void init(int num){
        n=num;
        cnt=0;
        rep(i,0,n)hd[i]=-1;
    }
    void adde(int a,int b,ll cap){
        ed[cnt].ne=hd[a];hd[a]=cnt;
        ed[cnt].to=b;
        ed[cnt].cap=cap;
        ed[cnt].flow=0;
        cnt++;
        ed[cnt].ne=hd[b];hd[b]=cnt;
        ed[cnt].to=a;
        ed[cnt].cap=0;
        ed[cnt].flow=0;
        cnt++;
    }
    bool bfs(int s,int t){
        rep(i,0,n)a[i]=0;
        while(!q.empty())q.pop();
        a[s]=INF;
        q.push(s);
        while(!q.empty()){
            int now=q.front();q.pop();
            for(int e=hd[now];~e;e=ed[e].ne){
                int nxt=ed[e].to;
                if(!a[nxt]&&ed[e].cap>ed[e].flow){
                    a[nxt]=min(a[now],ed[e].cap-ed[e].flow);
                    p[nxt]=e;
                    q.push(nxt);
                }
            }
            if(a[t])return 1;
        }
        return 0;
    }
    ll maxflow(int s,int t){
        ll res=0;
        while(bfs(s,t)){
            res+=a[t];
            for(int now=t;now!=s;now=ed[p[now]^1].to){
                ed[p[now]].flow+=a[t];
                ed[p[now]^1].flow-=a[t];
            }
        }
        return res;
    }
};

Dinic算法

首先重建分层图。

具体地,先BFS一遍分层,然后每个点只保留其到下一层的边,而删去其到上一层或同层的边。

随后,在分层图上,每次找到流量最大的增广流(即为阻塞流。这里的增广流是指整个流的路径并集,而非单条路径),将其加入答案、更新流。

直到不存在阻塞流为止,此时就是答案。

当前弧优化

这个优化可以保证复杂度。

具体地,我们维护每个点第一个还有余量的出边,防止每次都从头开始遍历。

表现在代码中,我们只需要在遍历边的时候加上引用即可。

但注意求新的阻塞流的时候需要复原。以上优化仅用于求同一阻塞流时。

模板

struct dinic{
	int n,ecnt;
	int hd[N],fr[N],ne[M<<1],to[M<<1];
	ll flow[M<<1],cap[M<<1];
	inline void init(int num){
		n=num,ecnt=0;
		rep(i,0,n)hd[i]=-1;
	}
	inline void adde(int a,int b,ll c){
		ne[ecnt]=hd[a],to[ecnt]=b,hd[a]=ecnt;
		flow[ecnt]=0,cap[ecnt]=c,++ecnt;
		ne[ecnt]=hd[b],to[ecnt]=a,hd[b]=ecnt;
		flow[ecnt]=0,cap[ecnt]=0,++ecnt;
	}
	int dp[N];queue<int> q;
	inline int bfs(int s,int t){
		rep(i,0,n)dp[i]=0;
		dp[s]=1;q.push(s);
		while(q.size()){
			int x=q.front();q.pop();
			for(int e=hd[x];~e;e=ne[e]){
				if(cap[e]>flow[e]&&!dp[to[e]])
					dp[to[e]]=dp[x]+1,q.push(to[e]);
			}
		}
		return dp[t];
	}
	ll dfs(int x,int t,ll f){
		if(x==t||!f)return f;
		ll res=0;
		for(int &e=fr[x];~e;e=ne[e]){
			if(cap[e]>flow[e]&&dp[to[e]]==dp[x]+1){
				ll r=dfs(to[e],t,min(cap[e]-flow[e],f-res));
				res+=r,flow[e]+=r,flow[e^1]-=r;
				if(res==f)break;
			}
		}
		return res;
	}
	inline ll maxflow(int s,int t){
		ll res=0;
		repl(i,0,ecnt)flow[i]=0;
		while(bfs(s,t)){
			rep(i,0,n)fr[i]=hd[i];
			res+=dfs(s,t,INF);
		}
		return res;
	}
};

最小割

问题

求容量最小的割。

解决

根据最大流最小割定理,最大流就是最小割。证明略。

方案

从源点开始,每次走有余量的边,走到的点都在 \(S\) 中。

费用流

问题

每条边多了一个属性单位费用 \(w\)。

流经的费用就是 \(f(u,v)\times w(u,v)\)。

\(w(u,v)=-w(v,u)\)

要求最大流前提下最小化费用的问题就是最小费用最大流。

SPP 算法

\(\text{SPP}\) 贪心算法,每次找增广路的时候优先找费用最小的即可。

SPFA

通过 \(\text{SPFA}\) 解决负边权最短路问题。注意不可以有负环。

直接把两个算法中找增广路部分(\(\text{BFS}\))换成 \(\text{SPFA}\) 即可。

EK模板

其他部分没有什么变化,最后回溯时顺便加上代价即可。

struct SPP_EK{
    int n,cnt;
    int hd[N];
    struct edge{
        int ne,to;
        ll cap,flow,val;
    }ed[M*2];
    int p[N];ll a[N];ll dis[N];
    queue<int> q;bool inq[N];

    void init(int num){
        n=num;
        cnt=0;
        rep(i,0,n)hd[i]=-1;
    }
    void adde(int a,int b,ll cap,ll val){
        ed[cnt].ne=hd[a];hd[a]=cnt;
        ed[cnt].to=b;
        ed[cnt].cap=cap;
        ed[cnt].flow=0;
        ed[cnt].val=val;
        cnt++;
        ed[cnt].ne=hd[b];hd[b]=cnt;
        ed[cnt].to=a;
        ed[cnt].cap=0;
        ed[cnt].flow=0;
        ed[cnt].val=-val;
        cnt++;
    }
    bool spfa(int s,int t){
        rep(i,0,n)a[i]=0,dis[i]=INF,inq[i]=0;
        a[s]=INF;dis[s]=0;
        q.push(s);
        inq[s]=1;
        while(!q.empty()){
            int now=q.front();q.pop();
            inq[now]=0;
            for(int e=hd[now];~e;e=ed[e].ne){
                int nxt=ed[e].to;
                if(dis[nxt]>dis[now]+ed[e].val&&ed[e].cap>ed[e].flow){
                    dis[nxt]=dis[now]+ed[e].val;
                    a[nxt]=min(a[now],ed[e].cap-ed[e].flow);
                    p[nxt]=e;
                    if(!inq[nxt])q.push(nxt),inq[nxt]=1;
                }
            }
        }
        return a[t];
    }
    pll maxflow_mincost(int s,int t){
        ll flow=0,cost=0;
        while(spfa(s,t)){
            flow+=a[t];
            for(int now=t;now!=s;now=ed[p[now]^1].to){
                ed[p[now]].flow+=a[t];
                ed[p[now]^1].flow-=a[t];
                cost+=ed[p[now]].val*a[t];
            }
        }
        return {flow,cost};
    }
};

Dinic模板

通过 \(dis\) 判断是否为最短增广路,每次只在最短增广路上增广。

struct dinic{
	int n,ecnt;
	int hd[N],fr[N],ne[M<<1],to[M<<1];
	ll flow[M<<1],cap[M<<1],val[M<<1];
	inline void init(int num){
		n=num,ecnt=0;
		rep(i,0,n)hd[i]=-1;
	}
	inline void adde(int a,int b,ll c,ll v){
		ne[ecnt]=hd[a],to[ecnt]=b,hd[a]=ecnt;
		flow[ecnt]=0,cap[ecnt]=c,val[ecnt]=v,++ecnt;
		ne[ecnt]=hd[b],to[ecnt]=a,hd[b]=ecnt;
		flow[ecnt]=0,cap[ecnt]=0,val[ecnt]=-v,++ecnt;
	}
	int dp[N];ll dis[N];bool inq[N];queue<int> q;
	inline int bfs(int s,int t){
		rep(i,0,n)dp[i]=0,dis[i]=INF;
		dp[s]=1;dis[s]=0;q.push(s);inq[s]=1;
		while(q.size()){
			int x=q.front();q.pop();inq[x]=0;
			for(int e=hd[x];~e;e=ne[e]){
				if(cap[e]>flow[e]&&dis[to[e]]>dis[x]+val[e]){
					dp[to[e]]=dp[x]+1,dis[to[e]]=dis[x]+val[e];
					if(!inq[to[e]])q.push(to[e]),inq[to[e]]=1;
				}
			}
		}
		return dp[t];
	}
	pll dfs(int x,int t,ll f){
		if(x==t||!f)return {f,0};
		ll res=0,cos=0;
		for(int &e=fr[x];~e;e=ne[e]){
			if(cap[e]>flow[e]&&dp[to[e]]==dp[x]+1&&dis[to[e]]==dis[x]+val[e]){
				auto [r,c]=dfs(to[e],t,min(cap[e]-flow[e],f-res));
				res+=r,flow[e]+=r,flow[e^1]-=r;
				cos+=c+r*val[e];
				if(res==f)break;
			}
		}
		return {res,cos};
	}
	inline pll maxflow_mincost(int s,int t){
		ll res=0,cos=0;
		repl(i,0,ecnt)flow[i]=0;
		while(bfs(s,t)){
			rep(i,0,n)fr[i]=hd[i];
			auto [r,c]=dfs(s,t,INF);
			res+=r,cos+=c;
		}
		return {res,cos};
	}
}dc;

上下界

无源汇

可行流

判断是否存在可行流量。

使用求最大流解决,我们考虑消去下界,这样问题就转化成了普通的网络流问题(还缺源点和汇点)。

具体的,对于每个点,上下界同时减去下界即可。

这时候我们发现流量不守恒了,计算出当前节点流量守恒需要的流量 \(d_i=(流入的下界之和-流出的下界之和\)),考虑构建一个附加源点和附加汇点,若 \(d_i<0\),则向汇点连一条 \(-d_i\) 容量的边,否则从源点连一条 \(d_i\) 容量的边,从而消去多出的出流量或入流量。

最后对附加源点到附加汇点跑一遍最大流即可,若所有附加边全部满流(判断附加源点的出边即可),则存在可行流。

此时每一条边的流量就是求出的流量加上它的下界。

有源汇

可行流

我们可以把这个问题转化成无源汇可行流问题。

具体的,连一条 \(t\rightarrow s\) 的上界为 \(\infty\) 下界为 \(0\) 的边,然后整个图就无源汇了。

若有解,那么可行流流量就是上面那条附加流的流量。

最大流

我们先跑一遍可行流。

这时候我们只需要榨干获取整个网络的剩余流量即可。

先删除所有附加边(事实上,只需要删除那个连接源点与汇点的边即可,因为别的附加边都满流了)。

然后在残量网络里用给出的源点和汇点再跑一遍最大流,二者相加即可。

最小流

类似地,我们退掉所有没有的流量就行。

更具体地,残量网络里跑一遍 \(t\rightarrow s\) 的最大流,可行流答案减去多余最大流就是最小流。

费用流

没有区别,把上面所有部分跑最大流的算法改成对应的费用流算法就行了。

模板(Dinic)

不推荐额外封装,直接在外部模拟,用上面的板子就行。

下面给出一个非费用流的封装示例。

struct BoundDinic{
    int n,cnt;
    int hd[N],fr[N];
    struct edge{
        int ne,to;
        ll low,cap,flow;
    }ed[M*2+N*2*2+2];
    int dp[N];
    queue<int> q;

    void init(int num){
        n=num;
        cnt=0;
        rep(i,0,n)hd[i]=-1;
    }
    void adde(int a,int b,ll low,ll cap){
        ed[cnt].ne=hd[a];hd[a]=cnt;
        ed[cnt].to=b;
        ed[cnt].low=low;ed[cnt].cap=cap;
        ed[cnt].flow=0;
        cnt++;
        ed[cnt].ne=hd[b];hd[b]=cnt;
        ed[cnt].to=a;
        ed[cnt].low=0;ed[cnt].cap=0;
        ed[cnt].flow=0;
        cnt++;
    }
    int bfs(int s,int t){
        rep(i,0,n)dp[i]=0;
        dp[s]=1;
        q.push(s);
        while(!q.empty()){
            int now=q.front();q.pop();
            for(int e=hd[now];~e;e=ed[e].ne){
                int nxt=ed[e].to;
                if(ed[e].cap>ed[e].flow&&!dp[nxt]){
                    dp[nxt]=dp[now]+1;
                    q.push(nxt);
                }
            }
        }
        return dp[t];
    }
    ll dfs(int now,int t,ll flow){
        if(now==t||!flow)return flow;
        ll res=0;
        for(int &e=fr[now];~e;e=ed[e].ne){
            int nxt=ed[e].to;
            if(ed[e].cap>ed[e].flow&&dp[nxt]==dp[now]+1){
                ll f=dfs(nxt,t,min(flow-res,ed[e].cap-ed[e].flow));
                res+=f;
                ed[e].flow+=f;
                ed[e^1].flow-=f;
                if(res==flow)break;
            }
        }
        return res;
    }
    ll maxflow(int s,int t){
        ll res=0;
        while(bfs(s,t)){
            rep(i,0,n)fr[i]=hd[i];
            res+=dfs(s,t,INF);
        }
        return res;
    }

    ll d[N];
    void LoopInit(int &s,int &t){
        s=n+1,t=n+2;
        hd[s]=hd[t]=-1;
        rep(i,0,cnt-1){
            if(i&1)d[ed[i].to]-=ed[i^1].low;
            else d[ed[i].to]+=ed[i].low;
            ed[i].cap-=ed[i].low;
        }
        rep(i,0,n){
            if(d[i]>0)adde(s,i,0,d[i]);
            if(d[i]<0)adde(i,t,0,-d[i]);
        }
        n+=2;
    }
    bool LoopPossibleFlow(){
        int s,t;
        LoopInit(s,t);
        maxflow(s,t);
        for(int e=hd[s];~e;e=ed[e].ne){
            if(ed[e].flow<ed[e].cap)return 0;
        }
        return 1;
    }

    int loop;
    ll RootPossibleFlow(int s,int t){
        loop=cnt;
        adde(t,s,0,INF);
        if(!LoopPossibleFlow())return -1;
        else return ed[loop].flow+ed[loop].low;
    }
    ll RootMaxFlow(int s,int t){
        ll res=RootPossibleFlow(s,t);
        if(res==-1)return -1;
        ed[loop].cap=ed[loop].flow=0;
        ed[loop^1].cap=ed[loop^1].flow=0;
        return res+maxflow(s,t);
    }
    ll RootMinFlow(int s,int t){
        ll res=RootPossibleFlow(s,t);
        if(res==-1)return -1;
        ed[loop].cap=ed[loop].flow=0;
        ed[loop^1].cap=ed[loop^1].flow=0;
        return res-maxflow(t,s);
    }
};
posted @ 2024-12-31 21:45  LastKismet  阅读(105)  评论(2)    收藏  举报