网络流6:费用流基础

网络流6 费用流基础(含消圈和原始对偶)

网络流3、4、5分别是最大流应用、最小割及其简单应用和最大流与最小割转化,但是我懒了还没写……

定义

(前置知识:网络流本体)

在费用流中,每条边除了容量外,还有一个“单位流量费用” $w(u,v) $。当 \((u,v)\) 的流量为 \(f(u,v)\) 时,其价值为 $f(u,v)\times w(u,v) $。

同前面对流量的扩展定义,价值也有这种性质,即 \(w(u,v)=-w(v,u)\)。这样一条边不论以谁为起点,都可以正确的计算价值。

做法

存在一个贪心的算法.它的思路是每次寻找单位费用最小的增广路进行增广,直到图上不存在增广路为止。可以用 spfa 实现。

但是,如果存在负圈,我们的 spfa 就会暴毙。此时,需要先使用消圈算法消去图上的负圈。

老样子证明懒了,详见 OI wiki

EK算法求费用流

dinic 算法在权值值域较大且均匀分布的情况下,是比 EK 要慢的,所以这里主要讲 EK。

本质就是将原本 EK 算法的增广路 dfs 函数改成了 spfa。

下面是最小费用最大流,对边权取负就可以当最大费用最大流

namespace Flow{
	#define Tpl int
	#define Tpw int
	struct Edge{
		int v,nx;Tpl c;Tpw w;
	}e[M*2];
	int hd[N],ecnt;
	void add(int u,int v,Tpl c,Tpw w){
		e[++ecnt]={v,hd[u],c,w};hd[u]=ecnt;
		e[++ecnt]={u,hd[v],0,-w};hd[v]=ecnt;
	}
	void init(){ecnt=-1;memset(hd,-1,sizeof(hd));}

	int S,T;

	Tpw dis[N];
	int pre[N];
	Tpl incf[N];
	queue <int> q;
	bool vis[N];
	bool spfa(){
		while(!q.empty()) q.pop();
		memset(dis,0x3f,sizeof(dis));
		memset(incf,0,sizeof(incf));
		memset(vis,0,sizeof(vis));
		incf[S]=INF,dis[S]=0,q.push(S);
		while(!q.empty()){
			int u=q.front();
			q.pop();
			vis[u]=0;
			for(int i=hd[u];~i;i=e[i].nx){
				int v=e[i].v;Tpw &w=e[i].w;Tpl &c=e[i].c;
				if(!c||dis[v]<=dis[u]+w) continue;
				dis[v]=dis[u]+w;
				incf[v]=min(incf[u],c);
				pre[v]=i;
				if(!vis[v]) q.push(v),vis[v]=1;
			}
		}
		return incf[T]>0;
	}

	pair<Tpl,Tpw> EK(int s,int t){
		S=s,T=t;
		Tpl flow=0;Tpw cost=0;
		while(spfa()){
			Tpl t=incf[T];
			flow+=t,cost+=(Tpw)t*dis[T];
			for(int i=T;i!=S;i=e[pre[i]^1].v){
				e[pre[i]].c-=t;
				e[pre[i]^1].c+=t;
			}
		}
		return {flow,cost};
	}
	int jiliguala=(init(),1);
	#undef Tpl
	#undef Tpw
}using Flow::init;using Flow::add;using Flow::EK;

最短路优化扩展&消圈算法

有些时候,我们会遇到负圈,由于增广路给予最短路,我们就会挂掉。

为了解决这种情况,我们可以先试着将负费用边流满,转化为有源汇上下界费用流。由于贪心,开局流满负圈边的情况下,我们不会重新造出来一个负圈,因此可以解决负圈的问题。

也顺便在此给出上下界费用流&消圈算法的模板(含 EK 本体和建图)

namespace Flow{
	#define Tpl int
	#define Tpw int
	struct Edge{
		int v,nx;Tpl c;Tpw w;
	}e[M*2];
	int hd[N],ecnt;
	void add(int u,int v,Tpl c,Tpw w){
		e[++ecnt]={v,hd[u],c,w};hd[u]=ecnt;
		e[++ecnt]={u,hd[v],0,-w};hd[v]=ecnt;
	}
	void init(){ecnt=-1;memset(hd,-1,sizeof(hd));}
	Tpl del(){Tpl c=e[ecnt].c;e[ecnt].c=e[ecnt^1].c=0;return c;}

	int S,T;

	Tpw dis[N];
	int pre[N];
	Tpl incf[N];
	queue <int> q;
	bool vis[N];
	bool spfa(){
		while(!q.empty()) q.pop();
		memset(dis,0x3f,sizeof(dis));
		memset(incf,0,sizeof(incf));
		memset(vis,0,sizeof(vis));
		incf[S]=INF,dis[S]=0,q.push(S);
		while(!q.empty()){
			int u=q.front();
			q.pop();
			vis[u]=0;
			for(int i=hd[u];~i;i=e[i].nx){
				int v=e[i].v;Tpw &w=e[i].w;Tpl &c=e[i].c;
				if(!c||dis[v]<=dis[u]+w) continue;
				dis[v]=dis[u]+w;
				incf[v]=min(incf[u],c);
				pre[v]=i;
				if(!vis[v]) q.push(v),vis[v]=1;
			}
		}
		return incf[T]>0;
	}

	pair<Tpl,Tpw> EK(int s,int t){
		S=s,T=t;
		Tpl flow=0;Tpw cost=0;
		while(spfa()){
			Tpl t=incf[T];
			flow+=t,cost+=(Tpw)t*dis[T];
			for(int i=T;i!=S;i=e[pre[i]^1].v){
				e[pre[i]].c-=t;
				e[pre[i]^1].c+=t;
			}
		}
		return {flow,cost};
	}
	int jiliguala=(init(),1);
	#undef Tpl
	#undef Tpw
}using Flow::init;using Flow::add;using Flow::EK;using Flow::del;

int W[N];

void mian()
{
	int s,t;
	read(n);read(m);read(s),read(t);
	int S=0,T=n+1;
	int resf=0,resw=0;
	while(m--){
		int u,v,w,c;
		read(u),read(v),read(c),read(w);
		if(w>0) add(u,v,c,w);
		else{
			add(v,u,c,-w);
			W[u]+=c,W[v]-=c;
			resw+=w*c;
		}
	}
	For(i,1,n){
		if(W[i]<0) add(S,i,-W[i],0);
		else add(i,T,W[i],0);
	}
	add(t,s,INF,0);
	auto p=EK(S,T);
	resf=del(),resw+=p.second;
	p=EK(s,t);
	resf+=p.first,resw+=p.second;
	printf("%d %d\n",resf,resw);
}

Primal-Dual 原始对偶算法

咕咕咕。 开始干活!

由于无良出题人会闲没事干发癫,可能会去卡 spfa。

关于 spfa,它死了。

为了解决这种情况,我们想要更换最短路算法。我们的第一反应肯定是换成 Dijkstra(dij) 算法。但网络上存在单位费用为负的边,因此无法直接使用 dij 算法。由此,我们引入了,万众瞩目的——Johnson 全源最短路(的思路)!

先简单介绍一下 Johnson 全源最短路的思路。我们随便找一个点作为源点,跑一个 Bellman-Ford(等能处理负圈边的)最短路,得到每个点的最短路 \(h_i\),然后将每个点的势能设置为 \(h_i\),并将边 \((u,v)\) 的边权设置为 $ w(u,v)'\leftarrow w(u,v)+h_u-h_v $。根据 $h_v\le h_u+d(u,v) $ 的性质,得知这样操作后边权为正。

假如说有一条最短路 $p_1\rightarrow p_2\rightarrow p_3\rightarrow p_4 \rightarrow \dots \rightarrow p_n $,那么其修改边权后的最短路长度为 $ d'(p_1, p_n) = (w(p_1, p_2) + h_{p_1} - {p_2}) + (w(p_2, p_3) + h_{p_2} - h_{p_3}) + (w(p_3, p_4) + h_{p_3} - h_{p_4}) + \dots + (w(p_{n-1}, p_n) + h_{p_{n-1}} - h_{p_n}) = d(p_1,p_n) + h_{p_1} - h_{p_n} $,也就是只与两点的势能有关。复杂度 $ O(nm \log m) $

我们通过为每个点设计一个势能,将网络上所有边的费用(下面简称为边权)全部变为非负值,从而用 dij 计算最短路。问题在于,每次增广后图的形态会发生变化,因此势能需要动态更新。

直接给出解决方案:每次跑完 dij 后,使用新的最短路 \(d_i\) 更新势能:$ h_i \leftarrow h_i+d_i $,这显然也能代表最短路,问题在于如何证明其边权仍为真。

证明类似:

  • 对于原有的边,增广前,有 $ d_i+(w(i,j)+h_i-h_j)-d_j \ge 0 $,因此 $w(i,j) + (h_i+d_i) - (h_j+d_j) \ge 0 $
  • 对于反向边,如果原本就存在则和上面相同,而新的边肯定来自于增广,有 $ d_i+(w(i,j)+h_i-h_j)-d_j = 0 $(否则不会在增广路上),和上文类似简单变形后能得出 $ d_i+(w(i,j)+h_i-h_j)-d_j = 0 $

综上,边权非负。

至此,我们学会了 Primal-Dual 原始对偶算法的所有内容,复杂度 $O(nm + fm \log m) $ 。如果图有特殊性质,第一次的最短路可以换成 bfs 之类的。

namespace Flow{
	#define Tpl int
	#define Tpw int
	struct Edge{
		int v,nx;Tpl c;Tpw w;
	}e[M*2];
	int hd[N],ecnt;
	void add(int u,int v,Tpl c,Tpw w){
		e[++ecnt]={v,hd[u],c,w};hd[u]=ecnt;
		e[++ecnt]={u,hd[v],0,-w};hd[v]=ecnt;
	}
	void init(){ecnt=-1;memset(hd,-1,sizeof(hd));}
	// Tpl del(){Tpl c=e[ecnt].c;e[ecnt].c=e[ecnt^1].c=0;return c;}//版子题就没有删边操作了

	int S,T;

	Tpw dis[N],h[N];
	int pre[N];
	Tpl incf[N];
	bool vis[N];

	queue <int> q;
	void spfa(){
		while(!q.empty()) q.pop();
		memset(dis,0x3f,sizeof(dis));
		memset(incf,0,sizeof(incf));
		memset(vis,0,sizeof(vis));
		incf[S]=INF,dis[S]=0,q.push(S);
		while(!q.empty()){
			int u=q.front();
			q.pop();
			vis[u]=0;
			for(int i=hd[u];~i;i=e[i].nx){
				int v=e[i].v;Tpw &w=e[i].w;Tpl &c=e[i].c;
				if(!c||dis[v]<=dis[u]+w) continue;
				dis[v]=dis[u]+w;
				incf[v]=min(incf[u],c);
				pre[v]=i;
				if(!vis[v]) q.push(v),vis[v]=1;
			}
		}
	}

	priority_queue <pair <Tpw,int> > pq;
	bool dij(){
		while(!pq.empty()) pq.pop();
		memset(dis,0x3f,sizeof(dis));
		memset(incf,0,sizeof(incf));
		memset(vis,0,sizeof(vis));
		incf[S]=INF,dis[S]=0,pq.push({0,S});
		while(!pq.empty()){
			int u=pq.top().second;pq.pop();
			if(vis[u]) continue;vis[u]=1;
			for(int i=hd[u];~i;i=e[i].nx){
				int v=e[i].v;Tpw w=e[i].w+h[u]-h[v];Tpl &c=e[i].c;
				if(!c||dis[v]<=dis[u]+w) continue;
				dis[v]=dis[u]+w;
				incf[v]=min(incf[u],c);
				pre[v]=i;
				pq.push({-dis[v],v});
			}
		}
		return incf[T]>0;
	}

	pair<Tpl,Tpw> Primal_Dual(int l,int r,int s,int t){
		S=s,T=t;
		spfa();
		For(i,l,r) h[i]=dis[i];
		Tpl flow=0;Tpw cost=0;
		while(dij()){
			Tpl t=incf[T];
			flow+=t,cost+=(Tpw)t*(dis[T]-h[S]+h[T]);
			for(int i=T;i!=S;i=e[pre[i]^1].v){
				e[pre[i]].c-=t;
				e[pre[i]^1].c+=t;
			}
			For(i,l,r) h[i]+=dis[i];
		}
		return {flow,cost};
	}
	int jiliguala=(init(),1);
	#undef Tpl
	#undef Tpw
}using Flow::init;using Flow::add;using Flow::Primal_Dual;

参考资料

我说 OI Wiki 神了有没有懂的 孩子们网络流写了 3 篇了,每次参考资料都有 OI Wiki。那你这不就是把 OI Wiki 的资料搬过来了吗

2

update on 7/14

更新了 Primal-Dual 原始对偶算法的内容。

posted @ 2026-07-14 20:51  FarrisL  阅读(3)  评论(0)    收藏  举报