【网络流相关】最大流和费用流的EK算法实现

Luogu P3376
最大流是网络流模型的一个基础问题。
网络流模型就是一种特殊的有向图。
概念:

  • 源点:提供流的节点,类比成为一个无限放水的水厂
  • 汇点:接受流的节点,类比成为一个无限收水的小区
  • 弧:类比为水管
  • 弧的容量:类比为水管的容量;用函数\(c(x,y)\)表示弧\((x,y)\)的容量
  • 弧的流量:类比为当前在水管中水的量;用函数\(f(x,y)\)表示弧\((x,y)\)的流量
  • 弧的残量:即容量-流量
  • 容量网络:对于一个网络流模型,每一条弧都给出了容量,则构成一个容量网络。
  • 流量网络:对于一个网络流模型,每一条弧都给出了流量,则构成一个流量网络。
  • 残量网络:对于一个网络流模型,每一条弧都给出了残量,则构成一个残量网络。最初的残量网络就是容量网络。

对于网络流模型\(G=(V,E)\)\(V\)为点集,\(E\)为边集)有如下性质:

  • 流量守恒:除了源点与汇点之外,流入任何节点的流一定等于流出该节点的流
  • 容量限制:\(\forall (x,y) \in E,有0<=f(x,y)<=c(x,y)\)
  • 斜对称性:\(\forall (x,y) \in E,有f(x,y)=-f(y,x).\)类似于函数奇偶性中的奇函数,或者是矢量的方向。

最大流问题,用通俗的方式解释就是从源点S到汇点T输送流量,询问最多有多少流量能输送到汇点。
对于这样的问题,我们引入一些新概念:

  • 增广路:一条从源点到汇点的路径\(R\),满足\(\forall (x,y) \in R, c(x,y)-f(x,y)>0.\)即残量为正数
  • 最大流最小割定理:网络流模型达到最大流,当且仅当残量网络中没有任何增广路(并不完整,但是足够了)

\(Ford-Fulkerson\)方法:每一次寻找一条增广路径。根据木桶原理,该增广路的最大流量\(f_{max}<=min(c(x,y)-f(x,y))\)。据此从源点发送流量至汇点并修改路径上所有弧的残量,直到无法找到增广路为止。
\(Edmons-Karp\)算法:基于\(Ford-Fulkerson\)方法的一种算法,核心就是利用\(BFS\)搜索源点到汇点的最短增广路,根据\(Ford-Fulkerson\)方法修改残量网络。复杂度最坏是\(O(nm^2)\)
所以其实我们在求最大流相关问题时,其实只利用到了残量网络,流量和容量一般并不需要记录。
关键点:有时候在求最大流时我们可能需要缩减一条边的流量,所以我们引入了反向边。当我们选用了一条反向边时,相当于缩减正向边的流量。很容易发现反向边的残量等于正向边的流量(最多恰好抵消正向流量)。
这样就能保证算法的正确性。

#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
struct data
{
	int to,next,val;
}e[2*100005];
int cnt,head[10005],prep[10005],pree[10005],flow[10005],ans;
queue<int> que;
int n,m,s,t,u,v,w;
void add(int u,int v,int w)
{
	e[++cnt].to=v;
	e[cnt].next=head[u];
	head[u]=cnt;
	e[cnt].val=w;
}
int bfs(int s,int t)
{
	while (!que.empty()) que.pop();
	flow[s]=0x3f3f3f3f;//flow记录的是在增广路上经过该点的流量
	que.push(s);
	for (int i=1;i<=n;i++) 
	{
		prep[i]=-1;//用于记录前驱节点
		pree[i]=0;//用于记录前驱边的编号
	}
	prep[s]=0;
	while (!que.empty())
	{
		int now=que.front();
		que.pop();
		if (now==t) break;
		for (int i=head[now];i;i=e[i].next)
		{
			if (e[i].val>0&&prep[e[i].to]==-1)
			{
				que.push(e[i].to);
				flow[e[i].to]=min(flow[now],e[i].val);
				pree[e[i].to]=i;
				prep[e[i].to]=now;
			}
		}
	}
	if (prep[t]!=-1) return flow[t];
	else return -1;
}
void EK(int s,int t)
{
	int delta=bfs(s,t);//寻找最短增广路的最大流量
	while (delta!=-1)
	{
		ans+=delta;
		for (int j=t;j;j=prep[j])
		{
			e[pree[j]].val-=delta;
			e[pree[j]^1].val+=delta;
			//链式前向星存边从编号2开始存储可以通过异或1快速取得反向边的编号。
		}
		delta=bfs(s,t);
	}
}
int main()
{
	scanf("%d%d%d%d",&n,&m,&s,&t);
	cnt=1;
	for (int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&u,&v,&w);
		add(v,u,0);
		add(u,v,w);
		//加入正反边
	}
	EK(s,t);
	printf("%d",ans);
	return 0;
}

Luogu P3381
假设现在对每一条弧加入一个费用,表示该弧单位流量需要的费用,要求最小费用最大流。
那么这就是费用流问题。
事实上并不难,对EK算法稍作修改,将EK中BFS寻找最短增广路改成SPFA寻找最小费用的增广路即可

#include<cstdio>
#include<queue>
using namespace std;
const int maxn=5005,maxm=50005,inf=0x3f3f3f3f;
struct data
{
	int to,next,val,pri;
}e[2*maxm];
int cnt,tot,ans,head[maxn],n,m,s,t,u,v,w,f,cost[maxn],prep[maxn],pree[maxn],flow[maxn];
void add(int u,int v,int w,int f)
{
	e[++cnt].to=v;
	e[cnt].next=head[u];
	e[cnt].val=w;
	e[cnt].pri=f;
	head[u]=cnt;
}
queue<int> que; 
int vis[maxn];
int SPFA(int s,int t)
{
	for (int i=1;i<=n;i++) 
	{
		cost[i]=inf;
		prep[i]=-1;
		pree[i]=0;
		flow[i]=inf; 
                //初始化容易漏
	}
	cost[s]=0;
	que.push(s);
	vis[s]=true;
	prep[s]=0;pree[s]=0;flow[s]=inf;
	while (!que.empty())
	{
		int now=que.front();
		que.pop();
		vis[now]=false;
		for (int i=head[now];i;i=e[i].next)
		{
			if (e[i].val>0&&cost[e[i].to]>cost[now]+e[i].pri)
			{
				cost[e[i].to]=cost[now]+e[i].pri;	
				flow[e[i].to]=min(flow[now],e[i].val);
				prep[e[i].to]=now;
				pree[e[i].to]=i;
				if (!vis[e[i].to]) 
				{
					vis[e[i].to]=true;
					que.push(e[i].to);
				}
			}
		}
	}
	if (prep[t]!=-1) return flow[t];
	else return -1;
}
void EK(int s,int t)
{
	int delta=SPFA(s,t);
	while (delta!=-1)
	{
		ans+=delta;tot+=delta*cost[t];
		for (int j=t;j;j=prep[j])
		{
			e[pree[j]].val-=delta;
			e[pree[j]^1].val+=delta;
		}
		delta=SPFA(s,t);
	}
}
int main()
{
	scanf("%d%d%d%d",&n,&m,&s,&t);
	cnt=1;
	for (int i=1;i<=m;i++)
	{
		scanf("%d%d%d%d",&u,&v,&w,&f);
		add(u,v,w,f);
		add(v,u,0,-f);//反向边的费用取相反数
	}
	EK(s,t);
	printf("%d %d",ans,tot);
	return 0;
}
posted @ 2019-11-30 11:29  Nanjo  阅读(386)  评论(0编辑  收藏  举报