介绍了网络流的基础知识,包括 Dinic 和 EK 算法。

网络和流

有一个有许多单向线路的铁路系统,每条线路的车票数量是有限的。现在有许多人在城市 \(S\),问这些人中最多有多少能到达城市 \(T\)

上面的问题就是网络流问题。在解决该问题之前,我们先来了解网络和流的严格定义。

问题中有许多单向线路的铁路系统就是一张网络:一个带权有向图 \(G(V,E)\)

问题中每条线路的车票数量是有限的,于是 \(G\) 中的边带边权,对于一条边 \((u,v,w) \in E\),记边 \((u,v,w)\)容量\(c(u,v) = w\)

在问题中,人们要从城市 \(S\) 到城市 \(T\)。一张网络中有源点汇点,记作 \(S\)\(T\),且满足 \(S,T \in V\)\(S \neq T\)

(Flow)是网络中的一个二元函数 \(f(a,b)\),且对所有 \(a,b \in V\) 均有定义。它表示实际流经该边的流量。在问题中,\(f(a,b)\) 表示 \(a\)\(b\) 线路的车票实际卖出的张数。

注意到,如果有 \(3\) 个人要从 \(a\)\(b\),有 \(5\) 个人要从 \(b\)\(a\),那么这等价于有 \(2\) 个人要从 \(b\)\(a\),因为他们的目的地相同。所以 \(f(a,b)\)\(f(b,a)\) 不会同时大于 \(0\),在这个例子中有 \(f(b,a)=2\)\(f(a,b)=-2\),因为「有 \(2\) 个人要从 \(b\)\(a\)」和「有 \(-2\) 个人要从 \(a\)\(b\) 是等价的」。

进一步可以发现 \(f(a,b) =-f(b,a)\)

于是我们可以得到 \(f\) 的完整定义:

\[f(a,b) = \begin{cases}f(a,b) & (a,b) \in E \\ -f(b,a) & (b,a) \in E \\ 0 & \operatorname{otherwise.}\end{cases} \]

显然地,流具有以下性质:

  • 容量限制

    \(f(u,v) \le c(u,v)\)

  • 流量平衡

    对于 \(u \in V\)\(u \neq S\)\(u \neq T\)\(\sum \limits_{(u,v) \in E} f(u,v)=0\),因为每个人要么留在 \(S\) 点不走要么最终到达 \(T\),对于中途节点,每个人到达之后肯定会再次出发。

  • 斜对称性

    \(f(a,b) = - f(b,a)\)

一条边 \((a,b)\)残余容量\(c_f(a,b)=c(a,b)-f(a,b)\)

一个网络的最大流为从 \(S\) 出发能流到 \(T\) 的最大流量。

增广路和 Edmonds-Karp 最大流算法

定义增广路为一条 \(S\)\(T\) 的路径 \(P=\{(u_1,v_1),(u_2,v_2),...(u_k,v_k)\}\),满足路径上每条边的残余容量均大于 \(0\)

显然,一条增广路能贡献的流量为 \(t=\min\{c_f(u_i,v_i)\}\)。同时路径上所有 \(f(u_i,v_i)\) 都增加了 \(t\)

似乎,当增广路不存在时,就找到了最大流。然而并不是这样的。

【瞎口胡】网络流基础_图论

上图中的最大流是 \(2\)。然而,如果找了另外一条增广路:

【瞎口胡】网络流基础_最大流_02

这个时候流就只有 \(1\) 了。这是因为第一条增广路堵塞了 \((4,T)\) 这条边。

我们建立反悔机制,建一个新图,给每条边建一条反向边,初始容量为 \(0\)。一条边每流过 \(1\) 单位的流量,就给这条边的容量减少 \(1\),再给反向边的容量增加 \(1\)

如果一条增广路可以经过反向边,那么说明之前一定有另外一条增广路经过了这条边。我们可以进行「反悔」,调整这两条增广路。

【瞎口胡】网络流基础_最大流_03

上图是两条增广路,其中绿色的增广路经过了反向边。然而这样的增广路在原图中并不存在,我们将它们调整为如下的两条增广路:

【瞎口胡】网络流基础_最大流_04

这样的增广路在原图中也是合法的,我们便找到了在原图中的一条增广路。

容易发现,在新图中,不存在增广路即代表找到了最大流。

Edmonds-Karp 算法(简称 EK 算法)每次使用 BFS 寻找增广路,直到增广路不存在为止。时间复杂度为 \(O(nm^2)\)。证明略,感兴趣的读者可以自行查阅资料。

一个找反向边的小技巧:将边从 \(2\) 开始标号,每次加边是原边和反向边使用相邻的编号,例如 \(2\)\(3\) 互为反向边,\(4\)\(5\) 互为反向边...这样 \(i\) 的反向边就是 \(i \operatorname{xor} 1\),其中 \(\operatorname{xor}\) 是按位异或。

inline bool bfs(void){
	memset(vst,false,sizeof(vst));
	memset(pre,0,sizeof(pre));
	while(!q.empty())
		q.pop();
	q.push(S);
	vst[S]=true;
	while(!q.empty()){
		int i=q.front();
		q.pop();
		for(int j=head[i];j;j=edge[j].next){
            int to=edge[j].to;
            if(!vst[to]&&edge[j].v){
				q.push(to),pre[to].drop=i,pre[to].edge=j,vst[to]=true; // 记录路径
				if(to==T)
					return true;
			}
        }
			
	}
	return false;
}
inline void E_K(void)
{
	int minx=0x7fffffff;
	while(bfs()){
		for(int i=T;i!=S;i=pre[i].drop) // 找到增广路上的最大流量
			minx=std::min(minx,edge[pre[i].edge].v);
		for(int i=T;i!=S;i=pre[i].drop) // 修改容量
			edge[pre[i].edge].v-=minx,edge[pre[i].edge^1].v+=minx;	
		maxsum+=minx;
	}
	return;
}

Dinic 最大流算法

在有多条相同长度相同的增广路时,EK 算法不能一次性处理它们。

Dinic 算法则对这一点做出了改进。按照到 \(S\) 的距离,图被分成若干层。

【瞎口胡】网络流基础_时间复杂度_05

在增广时,每一层的节点只往下一层送流。时间复杂度 \(O(n^2m)\)非常松的上界,几乎跑不满

inline bool bfs(void){
	std::queue <int> q=std::queue <int> ();
	memset(dis,INF,sizeof(dis));
	dis[S]=0;
	q.push(S);
	while(!q.empty()){
		int i=q.front();
		q.pop();
		for(int j=head[i];j;j=edge[j].next){
			if(edge[j].v&&dis[edge[j].to]>dis[i]+1)
				dis[edge[j].to]=dis[i]+1,q.push(edge[j].to);
		}
	} // 标号
	return dis[T]<INF;
}
int dfs(int i,int flow){
	if(i==T){
		return flow;
	}
	int maxsum=0;
	for(int j=head[i];j;j=edge[j].next){
		int to=edge[j].to;
		if(dis[to]!=dis[i]+1||!edge[j].v)
			continue;
		int res=dfs(to,std::min(flow,edge[j].v));
		flow-=res,edge[j].v-=res,edge[j^1].v+=res,maxsum+=res;
		if(!flow)
			break;
	}
	if(!maxsum) // 如果现在从这个节点出发找不到增广路,在**这一轮**增广中之后也一定找不到,将 dis 改为 INF 避免重复访问
		dis[i]=INF;
	return maxsum;
}

最小费用最大流

给每条边加上一个费用,可以看作车票有了一个价格。要求最大流,同时要费用最小,这就是最小费用最大流问题。

把反向边的权值改为原边权值的相反数,每次找 \(S\)\(T\) 的最短增广路进行增广即可。

时间复杂度 \(O(nmf)\),其中 \(f\) 是最大流。可以构造数据使得该算法被卡成指数级

然而大部分题目中网络是自行构造的,所以不需要过于担心被卡的问题。

inline bool spfa(void){
	std::queue <int> q=std::queue <int> ();
	q.push(S);
	vis[S]=true;
	memset(dis,INF,sizeof(dis));
	dis[S]=0;
	while(!q.empty()){
		int i=q.front();
		q.pop();
		vis[i]=false;
		for(int j=head[i];j;j=edge[j].next){
			int to=edge[j].to;
			if(!edge[j].v)
				continue;
			if(dis[to]>dis[i]+edge[j].w){
				dis[to]=dis[i]+edge[j].w;
				if(!vis[to])
					q.push(to),vis[to]=true;
			}
		}
	}
	return dis[T]<INF;
}
int dfs(int i,int flow){
	if(i==T){
		return flow;
	}
	vis[i]=true;
	int maxsum=0;
	for(int j=head[i];j;j=edge[j].next){
		int to=edge[j].to;
		if(!edge[j].v||vis[to]||dis[to]!=dis[i]+edge[j].w)
			continue;
		int res=dfs(to,std::min(edge[j].v,flow));
		flow-=res,maxsum+=res;
		edge[j].v-=res,edge[j^1].v+=res;
		cost+=res*edge[j].w; // 费用 = 单位费用 * 流量
		if(!flow){
			break;
		}	
	}
	if(!maxsum){
		dis[i]=INF;
	}
	vis[i]=false;
	return maxsum;	
}

网络流的当前弧优化

在单轮增广中,一旦我们经过了某一条边,那么它一定会尽可能地送流。

假设从 \(S\) 出发到点 \(u\) 处剩余 \(f\) 的流量。处理从点 \(u\) 出发的边时,如果某一条边被处理过了但 \(f\) 仍然不为 \(0\),那么说明这条边流完了所有能流的流量。

如果某一条边被处理之后 \(f\) 变为 \(0\),说明这条边之前的所有边流完了所有能流的流量,但这条边可能有剩余(也有可能流完了所有流之后 \(f\) 恰好为 \(0\)),下一次从这条边开始处理即可。

以最小费用最大流举例:

inline bool spfa(void){
	std::queue <int> q=std::queue <int> ();
	q.push(S);
	vis[S]=true;
	memset(dis,INF,sizeof(dis));
	dis[S]=0;
    for(int i=1;i<=n;++i)
        cur[i]=head[i]; // 复原 cur
	while(!q.empty()){
		int i=q.front();
		q.pop();
		vis[i]=false;
		for(int j=head[i];j;j=edge[j].next){
			int to=edge[j].to;
			if(!edge[j].v)
				continue;
			if(dis[to]>dis[i]+edge[j].w){
				dis[to]=dis[i]+edge[j].w;
				if(!vis[to])
					q.push(to),vis[to]=true;
			}
		}
	}
	return dis[T]<INF;
}
int dfs(int i,int flow){
	if(i==T){
		return flow;
	}
	vis[i]=true;
	int maxsum=0;
	for(int j=cur[i];j;j=edge[j].next){
        cur[i]=j; // 更新 cur,表示现在处理过的边
		int to=edge[j].to;
		if(!edge[j].v||vis[to]||dis[to]!=dis[i]+edge[j].w)
			continue;
		int res=dfs(to,std::min(edge[j].v,flow));
		flow-=res,maxsum+=res;
		edge[j].v-=res,edge[j^1].v+=res;
		cost+=res*edge[j].w;
		if(!flow){
			break;
		}	
	}
	if(!maxsum){
		dis[i]=INF;
	}
	vis[i]=false;
	return maxsum;	
}

练习题

Luogu P3376 【模板】网络最大流

Luogu P3381 【模板】最小费用最大流

Luogu P2740 [USACO 4.2]Drainage Ditches