专栏文章

最大流与Dijkstra做费用流

算法·理论参与者 47已保存评论 59

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
59 条
当前快照
1 份
快照标识符
@mhz5rlo3
此快照首次捕获于
2025/11/15 01:55
4 个月前
此快照最后确认于
2025/11/29 05:25
3 个月前
查看原文

0x00网络流简介

  网络流G=(V,E)G=(V, E)是一个有向图,其中每条边(u,v)(u, v)均有一个非负的容量值,记为c(u,v)0c(u, v)\geq0.如果(u,v)E(u, v)\notin E则可以规定c(u,v)=0c(u, v)=0.网络流中有两个特殊的顶点,即源点SS和汇点TT,源点可以提供无限的流量,而汇点可以接受无限的流量.
  与网络流相关的一个概念是流.设SS为网络的源点,TT为汇点,那么GG的流是一个函数f:V×VRf:V×V →R,满足以下性质:
  容量限制:u,vV\forall u,v∈V,满足f(u,v)c(u,v)f(u, v) \leq c(u, v);
  反对称性:u,vV\forall u,v∈V,满足f(u,v)=f(v,u)f(u, v) = - f(v, u);
  流守恒性:uVS,T\forall u∈V-{S, T},满足vVf(u,v)=0\sum_{v∈V}f(u,v)=0
  流ff的值定义为 f=vVf(s,v)|f|=\sum_{v\in V}f(s,v)   而最大流就是值最大的流.
  可以形象地理解为:源点是一个水库,汇点是一个用水量无限的用户,其它点是中转站,而边就是连接三者的水管.一个流就是一种合法的输水方案(每条水管运送水量在其容量之内,除了源点不会莫名其妙地出现水),它的值就是用户接受到的水量,而最大流就是用户接受水量最大的输水方案.
  残留网络是由可以容纳更多流的边组成的,即如果GG中的一条边没有满流,那么它就是残留网络GfG_f中的一条边.
  一条增广路则是指GfG_f中从SSTT的一条路径.

0x01最大流算法:以Dinic算法为例

以下定义:
w[i]w[i]为边ii的边权
w[i][j]w[i][j]为连接i,ji,j的边的边权
c[i]c[i]为边ii的费用 dis[i]dis[i]为起点到点ii的距离
SS为源点
TT为汇点
Dinic算法大致步骤:
1.用BFS给图标号,每个点的标号为到源点的最近距离(只走剩余容量大于0的边).
2.若BFS无法到达汇点则算法结束.
3.DFS一遍找出一条从源点到汇点的增广路,DFS时只走标号比当前点大1的点.
4.若没有这样的路径则返回1,否则将此路计入答案,并更新反向边,重复3.
反向边的概念:
假设有这样一个图(边上的数字代表其容量)
现在有一条2到5的路径
对于路径上每一条边(u,v),建立一条(v,u),容量为经过(u,v)的流量的边(图中绿色边)
假设又有一条6到1的边
那么最终的图是这样的(每条边上:(容量,流量))
例子:
BFS标号
DFS
更新反向边
由于无路可走,再BFS并DFS
更新反向边
无路可走,退出
最终:
PS:每条边上(容量,流量) Code(P3376)
CPP
#define out(i,a,now) for(int i=a.be[now],to=a.v[i];i;i=a.ne[i],to=a.v[i])
#define fo(i,a,b) for(i=a;i<=b;i++)
struct adj_list
{
	LL be[maxn],ne[maxm<<2],v[maxm<<2],w[maxm<<2],cnt;
	void init()
	{
		qk(be); cnt=1;
	}
	void add(LL x,LL y,LL z)
	{
		v[++cnt]=y;
		ne[cnt]=be[x];
		w[cnt]=z;
		be[x]=cnt;
	}
}v;
LL bfs()//标号
{
	queue<LL> q;
	q.push(t);
	qk(id);
	id[t]=1;
	while (!q.empty())
	{
		LL now=q.front();
		q.pop();
		if (now==s) break;
		out(i,v,now)
		if (v.w[i^1] && !id[to])
		{
			id[to]=id[now]+1;
			q.push(to);
		}
	}
	return id[s];//判断是否有可行流
}
LL dfs(LL now,LL flow)//寻找可行流
{
	if (now==t) return flow;
	LL miflow;
	out(i,v,now)
	{
		LL wei=v.w[i];
		if (wei && id[to]==id[now]-1)
		{
			miflow=dfs(to,min(flow,wei));
			if (miflow)
			{
				v.w[i]-=miflow;
				v.w[i^1]+=miflow;//增加反向边流量
				return miflow;
			}
		}
	}
	return 0;
}
LL dinic()
{
	LL res=0;
	while (bfs())
	{
		LL temp;
		while (temp=dfs(s,LLONG_MAX>>1))
			res+=temp;
	}
	return res;
}
int main()
{
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
	LL i;
	LL x,y,z;
	v.init();
	fo(i,1,m)
	{
		scanf("%lld%lld%lld",&x,&y,&z);
		v.add(x,y,z);
		v.add(y,x,0);
	}
	LL ans=dinic();
	printf("%lld",ans);
}

0x02最小费用最大流算法

  现在你需要为流经一条边ii的单位流量支付费用c[i]c[i],其实只需将最大流算法中的DFS改为求最短路,找到一条从源点到汇点每条边费用之和最小的路并更新答案.
  鉴于现在SPFA人人喊打,以下给出一种使用dijkstra算法的方法.
  由于费用可能为负数,普通的dijkstra算法无法实现.因此我们考虑对原图做一次SPFA求出源点到每个点的最短路,之后我们为每个点ii赋点权h[i]h[i]dis[i]dis[i],并视每条连接u,vu,v的边ii的边权w[i]w'[i]w[i]+h[u]h[v]w[i]+h[u]-h[v],由于对于任意两点u,vu,v,有h[u]h[v]>=w[u][v]h[u]-h[v]>=w[u][v],所以w[i]>=0w'[i]>=0,这样一来新图上的dis[i]dis'[i]就等于dis[i]+h[S]h[i]dis[i]+h[S]-h[i](对于路径上除起点和终点以外的点ii,其入边的h[i]-h[i]与出边的+h[i]+h[i]抵消),由于每次跑最短路时h[i]h[i]都是不变的,所以求出了dis[i]dis'[i]也就求出了dis[i]dis[i](dis[i]=dis[i]h[S]+h[i]dis[i]=dis'[i]-h[S]+h[i],其实很显然h[S]=0h[S]=0)
  但是跑完之后需要加入反向边,原来的h[i]h[i]可能会不适用,所以我们需要更新h[i]h[i] 对于最短路上每一条连接(u,v)(u,v)的边,显然有dis[u]+w[u][v]=dis[v]dis'[u]+w'[u][v]=dis'[v] 从而 dis[u]+h[u]h[v]+w[u][v]=dis[v]dis'[u]+h[u]-h[v]+w[u][v]=dis'[v] (dis[u]+h[u])(dis[v]+h[v])+w[u][v]=0(dis'[u]+h[u])-(dis'[v]+h[v])+w[u][v]=0 w[u][v]=w[v][u]∵w[u][v]=-w[v][u] (dis[v]+h[v])(dis[u]+h[u])+w[v][u]=0∴(dis'[v]+h[v])-(dis'[u]+h[u])+w[v][u]=0 所以我们只要对于每个点iih[i]h[i]加上dis[i]dis'[i]即可.
由于
dis[i]+h[i]=dis[i]+h[S]h[i]+h[i]=dis[i]+h[S]dis'[i]+h[i]=dis[i]+h[S]-h[i]+h[i]=dis[i]+h[S]
h[S]又是一个常量(就是00) 所以h[i]h[i]还是一开始我们所希望的dis[i]dis[i],对于每个点iih[i]h[i]加上dis[i]dis'[i]不会破坏每条边边权不为负的特性.
CPP
#define out(i,a,now) for(int i=a.be[now],to=a.v[i];~i;i=a.ne[i],to=a.v[i])
#define fo(i,a,b) for(i=a;i<=b;i++)
struct adj_list
{
	LL be[maxn],ne[maxm<<4],v[maxm<<4],w[maxm<<4],c[maxm<<4],cnt;
	void init()
	{
		memset(be,-1,sizeof(be));
		cnt=0;
	}
	void add(LL pre,LL nxt,LL wei,LL cost)
	{
		v[cnt]=nxt;
		ne[cnt]=be[pre];
		be[pre]=cnt;
		w[cnt]=wei;
		c[cnt]=cost;
		++cnt;
	}
};
struct NetWork
{
	adj_list v;
	LL dis[maxn],h[maxn],pre_node[maxn],pre_edge[maxn],inque[maxn];
	void SPFA(LL be,LL en)
	{
		queue<LL> q;
		qk(inque);
		LL i;
		fo(i,1,n) h[i]=LLONG_MAX>>1;
		h[be]=0; inque[be]=1;
		q.push(be);
		while (!q.empty())
		{
			LL now=q.front();
			q.pop();
			out(i,v,now)
			if (h[now]+v.c[i]<h[to] && v.w[i])
			{
				h[to]=h[now]+v.c[i];
				if (!inque[to])
				{
					q.push(to);
					inque[to]=1;
				}
			}
			inque[now]=0;
		}
	}
	void MCMF(LL s,LL t,LL &flow,LL &cost)
	{
		flow=cost=0;
		SPFA(s,t);//用一次SPFA更新h数组
		while (1)
		{
			pre_node[s]=pre_edge[s]=-1;//开始堆优化dijkstra算法
			LL i;
			fo(i,1,n) dis[i]=LLONG_MAX>>1;
			dis[s]=0;
			priority_queue<Pair,vector<Pair>,greater<Pair> > pq;
			pq.push(mp(0,s));
			while(!pq.empty())
			{
				Pair now=pq.top();
				pq.pop();
				if (now.first!=dis[now.second]) continue;
				if (now.second==t) break;
				out(i,v,now.second)
				{
					LL len=v.c[i]+h[now.second]-h[to];
					if (v.w[i] && dis[now.second]+len<dis[to])
					{
						dis[to]=dis[now.second]+len;
						pq.push(mp(dis[to],to));
						pre_node[to]=now.second;//记录从哪个点来
						pre_edge[to]=i;//记录从哪条边来
					}
				}
			}
			if (dis[t]>=(LLONG_MAX>>1)) break;
			LL miflow=LLONG_MAX>>1;
			for(i=t;i!=s;i=pre_node[i])
				cmin(miflow,v.w[pre_edge[i]]);//找到路径上流量最小值
			for(i=t;~i;i=pre_node[i])
			{
				v.w[pre_edge[i]]-=miflow;
				v.w[pre_edge[i]^1]+=miflow;//更新反向边
			}
			flow+=miflow;
			cost+=miflow*(h[t]+dis[t]);
			fo(i,1,n) h[i]=min(h[i]+dis[i],LLONG_MAX>>1);//更新h数组
		}
	}
	void sol()
	{
		v.init();
		LL i,x,y,z,w;
		scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
		fo(i,1,m)
		{
			scanf("%lld%lld%lld%lld",&x,&y,&z,&w);
			v.add(x,y,z,w);
			v.add(y,x,0,-w);
		}
		LL flow,cost;
		MCMF(s,t,flow,cost);
		printf("%lld %lld",flow,cost);
	}
}solver;
int main()
{
	solver.sol();
	return 0;
}

0x03最小割最大流定理

  GG的一个割把点集VV分为两部分,其中源点S属于其中一部分,汇点T属于另外一部分.割是GG的边集EE的一个子集,每条边的两个端点分别属于不同的点集.割的容量定义为割的每条边的容量和,最小割就是容量最小的一个割.
  例子:
  
  黄色的边就是一个割
  介绍一下网络流中最重要的定理之一------最小割最大流定理:
  如果ffGG中一个流,则以下条件是等价的:
  1.ffGG的一个最大流
  2.残留网络GfG_f不包含增广路
  3.存在一个割,f|f|等于其容量
  那么如何借此求出最小割呢?我们看可以先求出最大流,以S为起点在残留网络上DFS,搜索到的点就是包含S的一部分,剩余的则是另一部分,而最小割即是连接两部分的所有边.

0x04典型例题

1.飞行员配对方案问题(P2756)
  一个二分图最大匹配问题,源点向所有外籍飞行员连容量为1的边,外籍飞行员向可以配对的英国飞行员连容量为1的边,英国飞行员向汇点连容量为1的边,网络最大流即是答案,若外籍飞行员连向英国飞行员的边满流,说明这一对被选中.
  Code
2.太空飞行计划问题(P2762)
  一个最大权闭合图问题,详细说明可以见胡伯涛的《最小割模型在信息学竞赛中的应用》.
  简要说一下方法:源点向实验连容量为利润的边,实验向所需器材连容量为无穷大的边,器材向汇点连容量为花费的边,所有实验利润之和减去网络最小割的容量即是答案,方案可以按照上文方法输出.
  Code
3.最小路径覆盖问题(P2764)
  寻找最小路径条数等价于为尽可能多的点找到后继,于是就可以用一种类似于二分图匹配的方法求解.将点ii拆为i1,i2i_1,i_2,源点向所有i1i_1连容量为1的边,所有i2i_2向汇点连容量为1的边,若原图上有边(u,v)(u,v),则u1u_1v2v_2连容量为1的边.答案为总点数减去网络最大流(网络最大流就是找到了后继的点数),若u1u_1v2v_2的边满流,说明路径上vvuu的后继.
  Code

评论

59 条评论,欢迎与作者交流。

正在加载评论...