网络最大流、费用流及基本算法

网络最大流指的是这样一类问题:给定一张 n 个点,m 条边的有向图和一个源点 \(s\),一个汇点 \(t\),满足 \(s\) 的入度为零,\(t\) 的出度为零。每条边有一个最大流量,即可以通过该边的最大人数(可以类比一下交通系统)。需要求出从源点流向汇点的最大流量。最小费用最大流(亦称费用流)则是在其基础上对每条边有一个单位流量花费的限制,如果一条边 \(i\) 花费为 \(c_i\),流量为 \(f_i\),则要求最大流的基础上总费用 \(\sum_{e\in E}c_e*f_e\) 最小。

现在常用的最大流解法有 Edmond-Karp 动能算法(EK 算法),Dinic,ISAP,Push-Relabel 预流推进算法,HLPP(High Level Preflow Push,最高标号预流推进法,高标预流,人称欢乐跑跑),一下介绍我认为最有用的一种,Dinic

注意 网络流的实现中最重要的就是暴力,所以所有网络流算法可能都或多或少地让人感到不适,且复杂度都非常高,所以请忘记以前其它图论算法的简单优雅。

反向边指的是原来有向图中的所有边的反向,初始时可行流量均为零,Dinic 算法中每确定要流过一条边,则把这条边的流量减少后在反向边上增加流量。增广路指的是在当前已经进行过许多边和反向边的可行流量增减后仍然连通 \(s\)\(t\) 且可行流量不为零的一条路径。Dinic 算法基于这样一个基本思想,即每次寻找增广路,然后寻找增广路上的流量最小的一条边,然后把所有增广路上的边的可行流量都减去最小流量,反向边都加上最小流量,重复此过程直到不存在增广路。

这个算法为什么正确呢?我们需要明白为什么需要反向边。最重要的一点是需要给每条边一个“反悔”的机会。比如这张图:

假设我们第一次走了 s->2->1->t 这条边,则

那么之后再走 s->1->t 时,就只能有 10 的流量

很显然是不优的。

我们再来看有了反向边的情况:

我们第一次依然走 s->2->1->t 这条边,则

下一次我们走了 s->1->2->t

我们会发现,这时候 s->1->t 和 s->2->t 都是可行的了,而且所有流量都是合法的。换言之,如果我们流过了正向边,再流过了反向边,则相当于两边的流都各自找了出口而未流经这条边,从而有了一个“反悔”的操作。而且由于每次都会贡献新的答案,所以不需要担心会反复在一条边上流来流去。只要能增广就增广,一定正确。

实际操作中,最大的复杂度来自于每次 BFS 寻找是否存在增广路然后增广。那么如果我们每次多选择一些可行的增广路就可以跑得更快,这就是多路增广优化。而对于每个顶点延伸出去的所有边,我们一定是跑过了它可能的最大价值才跳转到下一条边的,而多路增广时我们下一次再遇到它就可以直接从上次已经枚举到了的边进行枚举。换言之,我们可以构建一个 \(f_2\) 数组,初始时它等于 \(first\) 数组。之后每次增广从他开始,每次遇到一条边就把它更新为新的边。经过这两个优化后 Dinic 可以达到 \(O(n^2m)\),而且在随机图中的表现十分优秀,二分图中可以达到 \(O(m\sqrt n)\)

实际上还有一个用 LCT 把 Dinic 优化到 \(O(nm\log n)\) 的方法,不过那个太玄学了,而且应该没有毒瘤出题人会专门卡 Dinic 吧(逃

洛谷板子题

#include<cstdio>
#include<queue>
#include<cstring>

typedef long long ll;
const int maxn = 205;
const int maxm = 5E+4 + 5;
const int INF = 0x3f3f3f3f;

int n, m, s, t, tot = 1;                                               //注意不能从零开始
int f2[maxn], first[maxn];
int dis[maxn];
std::queue<int> q;
struct Edge {
	int to, next; ll w;
} e[maxm * 2];

inline void Add(int x, int y, ll w) {
	e[++tot] = { y, first[x], w };
	first[x] = tot;
}

bool BFS() {
    memset(dis, INF, sizeof dis);
      
    q.push(s), dis[s] = 0;
    while(!q.empty()) {
          int u = q.front(); q.pop();
          for(int i = first[u]; i; i = e[i].next) {
                int v = e[i].to;
                if(dis[v] > dis[u] + 1 && e[i].w > 0)
                      dis[v] = dis[u] + 1, q.push(v);
          }
    }
      
    return dis[t] < INF;
}

ll DFS(int u, ll flow) {
	if(u == t) return flow;
	
	ll res = 0;
	for(int &i = f2[u]; i; i = e[i].next) {                        //当前弧优化,注意那个引用
		int v = e[i].to;
		
		if(dis[v] != dis[u] + 1 || !e[i].w) continue;          //每次只走深度相差为 1 的,防止出现死循环
		ll d = DFS(v, std::min(flow, e[i].w));
		
		e[i].w -= d, e[i ^ 1].w += d;
		res += d, flow -= d;
		if(!flow) break;
	}
	
	if(flow) dis[u] = -1;                                          //如果仍然有剩余流量,则当前节点已经没有剩余价值了
	return res;
}

ll Dinic() {
	ll res = 0, x;
	while(BFS()) {
		memcpy(f2, first, sizeof f2);
		while(x = DFS(s, INF)) res += x;
	}
	return res;
}

int main() {
	scanf("%d%d%d%d", &n, &m, &s, &t);
	for(int i = 1, x, y, w; i <= m; ++i) {
		scanf("%d%d%d", &x, &y, &w);
		Add(x, y, w), Add(y, x, 0);
	}
	printf("%lld", Dinic());
}

费用流的常用算法是 SPFA 类 Dinic 法(就是那个最短路的 SPFA),具体来说就是把反向边的花费定为正向边的相反数,然后把 Dinic 的 BFS 改成 SPFA(也可以用 Primal-Dual 原始对偶算法跑一遍 SPFA 后跑 dijkstra),把原来的两点之间距离 1 改成花费。然后就是如果 n 比较大,需要专门开一个 bool 型数组来记录是否已经 DFS 过某个点了,虽然这样会增大时间,但有效地防止了 MLE。

洛谷板子题

#include<cstdio>
#include<queue>
#include<cstring>

typedef long long ll;
const int maxn = 5005;
const int maxm = 5E+4 + 5;
const int INF = 0x3f3f3f3f;

int n, m, s, t, tot = 1;
ll maxFlow, minCost;
int f2[maxn], first[maxn], dis[maxn];
bool vis[maxn];
std::queue<int> q;
struct Edge {
	int to, next; ll w, cost;
} e[maxm * 2];

inline void Add(int x, int y, ll w, ll cost) {
	e[++tot] = { y, first[x], w, cost };
	first[x] = tot;
}

bool SPFA() {
    memset(dis, INF, sizeof dis);
    
    q.push(s), dis[s] = 0;
    while(!q.empty()) {
          int u = q.front(); q.pop();
          for(int i = first[u]; i; i = e[i].next) {
                int v = e[i].to;
                if(dis[v] > dis[u] + e[i].cost && e[i].w > 0)
                      dis[v] = dis[u] + e[i].cost, q.push(v);
    }    
    return dis[t] < INF;
}

ll DFS(int u, ll flow) {
	if(u == t) return flow;
	
	vis[u] = 1;                        //请注意这一行,这一行保证了不会重复选到已选的点,从而保证了不会爆栈 MLE
	
	ll res = 0;
	for(int &i = f2[u]; i; i = e[i].next) {
		int v = e[i].to;
		
		if(vis[v] || dis[v] != dis[u] + e[i].cost || !e[i].w) continue;
		ll d = DFS(v, std::min(flow, e[i].w));
		
		e[i].w -= d, e[i ^ 1].w += d;
		res += d, flow -= d, minCost += d * e[i].cost;
		if(!flow) break;
	}
	
	if(flow) dis[u] = -1;
	return vis[u] = 0, res;
}

ll Dinic() {
	ll res = 0, x;
	while(SPFA()) {
		memcpy(f2, first, sizeof f2);
		while(x = DFS(s, INF)) res += x;
	}
	return res;
}

int main() {
	scanf("%d%d%d%d", &n, &m, &s, &t);
	for(int i = 1, x, y, w, c; i <= m; ++i) {
		scanf("%d%d%d%d", &x, &y, &w, &c);
		Add(x, y, w, c), Add(y, x, 0, -c);
	}
	
	maxFlow = Dinic();
	printf("%lld %lld\n", maxFlow, minCost);
}
posted @ 2020-08-23 00:37  whx1003  阅读(658)  评论(0编辑  收藏  举报