【网络流】总结

网络

又称流网络 ( F l o w N e t w o r k ) (Flow Network) (FlowNetwork),一个流网络 G = ( V , E ) G = (V, E) G=(V,E) 为一个有向图,其中每一条边 ( u , v ) ∈ E (u, v) \in E (u,v)E 都有非负权值 c ( u , v ) c(u, v) c(u,v),记为流量 ( C a p a c i t y ) (Capacity) (Capacity)

网络中有两个特殊的点,源点 s   ( S o u r c e ) s \ (Source) s (Source) 及汇点 t   ( S i n k ) t \ (Sink) t (Sink)。顾名思义,源点即流发源的点,入度为 0,汇点即流汇集的点,出度为 0。

对于二元组 u ∈ V   , e ∈ V u \in V \ ,e \in V uV ,eV,有实数函数 f ( u , v ) f(u, v) f(u,v),满足:

  • 容量限制:对于每条边,流经该边的流量不得超过该边的容量,即 f ( u , v ) < c ( u , v ) f(u, v) < c(u, v) f(u,v)<c(u,v)

  • 斜对称性:每条边的流量与其相反边的流量之和为 0,即 f ( u , v ) = − f ( v , u ) f(u,v) = -f(v, u) f(u,v)=f(v,u),一个方向的流是其反方向的流的相反数。

  • 流守恒性:除源点和汇点,流入一个点的流量要等于流出这个点的流量,即对任意 u u u,一定有 ∑ ( u , v ) ∈ E , f ( u , v ) = 0 \sum _{(u,v)∈E},f(u,v)=0 (u,v)E,f(u,v)=0。可以理解为 u u u 点本身不会创造和消耗流量。

一般可以把网络流理解为整个图的流量。即 f = ∑ v ∈ V f ( s , v ) f = \sum _{v \in V} f(s,v) f=vVf(s,v)

剩余(残留)网络

对于一条边 ( u , v ) ∈ E (u,v) \in E (u,v)E,边的残留容量 r ( u , v ) = r(u,v) = r(u,v)= c ( u , v ) − f ( u , v ) c(u, v) - f(u, v) c(u,v)f(u,v)

残留网络:对于残留网络 G f ( V , E f ) G_f(V,E_f) Gf(V,Ef) E f = { ( u , v ) ∣ r ( u , v ) > 0 } E_f = \left \{ (u, v) \mid r(u,v) > 0 \right \} Ef={(u,v)r(u,v)>0}

G G G 中所有结点和残留容量大于 0 的边构成的图。

最大流

指在网络中指定一个合法的流,使其最大化。

F F FF FF 增广

F F FF FF ( F o r d – F u l k e r s o n ) (Ford–Fulkerson) (FordFulkerson) 增广方法运用贪心的思想,通过寻找增广路来更新并求解最大流。

增广路:指 G f G_f Gf ,即残留网络上的一条从源点 s s s 到汇点 t t t 的路径 P P P,在残留网络中,我们给这条路径上的边都减去等量的流量,也就是在原网络中加上了等量的流量。

这条路径上可修改的残留流量最大值就为 m i n { r ( u , v ) ∣ ( u , v ) ∈ P } min \left \{ r(u,v) \mid (u,v ) \in P \right \} min{r(u,v)(u,v)P},显然修改后就一定有边的残留流量变为了 0,也就不存在这条路径了。

F F FF FF 增广就是将最大流的求解视为若干次增广分别得到的流的叠加。

在进行增广时,为满足流的斜对称性,需要在一条边减去流量时在其反向边上加上等量流量,我们可以通过引入例子解释原因:

在这里插入图片描述
可以发现如果没有反向边,可能就会忽略一些更优的情况。

C o d e Code Code

int dfs(int u, int now_f) {
    if (u == en) {
        return now_f;
    }
    vis[u] = 1;
    for (int i = he[u]; i; i = ed[i].nx) {
        int v = ed[i].to, c = ed[i].c;
        if (c > 0 && !vis[v]) {
            int res = dfs(v, min(now_f, c));
            if (res == -1) {
                continue;
            }
            ed[i].c -= res;
            ed[i ^ 1].c += res;
            return res;
        }
    }
    return -1;
}

void F_F() {
    ans = 0;
    while ((f = dfs(st, inf)) != -1) {
        ans += f;
        memset(vis, 0, sizeof(vis));
    }
    printf("%d", ans);
}

时间复杂度: O ( e f ) O(ef) O(ef) e e e 为边数, f f f 为最大流。因为一次 d f s dfs dfs 的时间复杂度为 O ( e ) O(e) O(e),而每一次最少更新 1 单位流量。

E K EK EK 算法

E K EK EK ( E d m o n d s – K a r p ) (Edmonds–Karp) (EdmondsKarp) 算法,是一种基于 F F FF FF 增广,使用 b f s bfs bfs 具体实现的网络流算法。

算法流程:

  • s s s 出发,通过 b f s bfs bfs 寻找到一条从 s s s t t t 的路径。

  • 在寻找路径的同时,记录下路径上每一节点的前一节点以及更新残留流量的最小值,在找到路径返回时更新残留流量。

  • 重复操作直到残留网络中没有从 s s s t t t 的路径存在。此时更新的残留容量总数即为最大流。

C o d e Code Code

ll bfs() {
    ll u;
    queue<ll> q;
    memset(fa, 0, sizeof(fa));
    memset(fu, 0, sizeof(fu));
    memset(aug, 0, sizeof(aug));
    aug[st] = inf;
    q.push(st);
    while (q.size()) {
        u = q.front();
        q.pop();
        for (ll i = he[u]; i; i = ed[i].nx) {
            ll v = ed[i].to, c = ed[i].c;
            if (aug[v] == 0 && c > 0) {//此处的aug数组既可以记录最小残留容量,又可以当成vis数组使用
                q.push(v);
                aug[v] = min(aug[u], c);
                fa[v] = u;
                fu[v] = i;
                if (v == en) {
                    return aug[en];
                }
            }
        }
    }
    return 0;
}

void E_K() {
    ans = 0;
    while (1) {
        ll del = bfs();
        if (!del) {
            break;
        }
        ll v, u, w;
        for (v = en; v != st; v = u) {
            u = fa[v];
            w = fu[v];
            ed[w].c -= del;
            ed[w ^ 1].c += del;
        }
        ans += del;
    }
    printf("%lld", ans);
}

时间复杂度: O ( v ∗ e 2 ) O(v*e^2) O(ve2),其中 v v v 为点数, e e e 为边数,证明过程可参考 t h i s this this

D i n i c Dinic Dinic 算法

在增广之前先对 残留网络 G f G_f Gf 分层,使得一个点只能流向下一层的节点。在此分层图上寻找一条流量最大的增广路,那么此时我们更新路径上的边流量,再去重新分层,寻找路径。重复以上过程我们即可找出最大流。

此时引入一个优化:当前弧优化。

如果一个节点有多条出边,那么在一次 d f s dfs dfs 中寻找路径时到同一点都从头遍历就会极大浪费时间,因为存在有些已经更新过,没有利用价值的边,那么我们就可以用一个 n o w now now 数组存下第一条有价值的边开始遍历。

C o d e Code Code

int bfs() {
	queue<int> q;
	memset(de, 0, sizeof(de));
	de[st] = 1;
	q.push(st);
	now[st] = he[st];
	while (q.size()){
		int u = q.front();
		q.pop();
		for (int i = he[u]; i; i = ed[i].nx){
			int v = ed[i].to;
			if (!de[v] && ed[i].c > 0){
				now[v] = he[v];
				de[v] = de[u] + 1;
				if (v == en){
					return 1;
				}
				q.push(v);
			}
		}
	}
	return 0;
}

int Dinic(int u, int flow) {
	if (u == en){
		return flow;
	}
	int res = flow;
	for (int i = now[u]; i && res; i = ed[i].nx){
		int v = ed[i].to, c = ed[i].c;
		now[u] = i;
		if (de[v] == de[u] + 1 && c > 0){
			int k = Dinic(v, min(res, c));
			if (!k){
				de[v] = 0;
			}
			ed[i].c -= k;
			ed[i ^ 1].c += k;
			res -= k;
		}
	}
	return flow - res;
}

例题

Pigs 猪

可以看出题目中有几个限制:

每个猪舍有定量的猪,顾客有最多能购买的猪头数。

顾客会根据顺序依次来到猪舍,顾客可以重新分配他能打开的猪舍中猪的头数。

所以考虑建立超级源点及汇点,以每一个顾客为节点,向汇点连他能购买的最多猪数。

因为每一个顾客如果不是第一次打开一个猪舍,那么就可以理解为他将吃上一个打开这个猪舍的顾客买剩下的,就将上一个顾客向此顾客连一条容量为无限大的边。如果是第一次打开,那么它将拥有购买的优先级,直接向源点连一条容量为此猪舍中猪头数的边。跑一个最大流即为答案。

核心代码:

st = 0, en = n + 1;
for (int i = 1; i <= n; i ++){
	int p;
	scanf("%d", &p);
	for (int j = 1; j <= p; j ++){
		int u;
		scanf("%d", &u);
		if (!mp[u]){
			if (ma[make_pair(st, i)]){
				int ot = ma[make_pair(st, i)];	
				ed[ot].c += co[u];
			}
			else {
				A_Edge(st, i, co[u]);
				ma[make_pair(st, i)] = tot;
				A_Edge(i, st, 0);
			}
		}
		else if (!ma[make_pair(mp[u], i)]){
			ma[make_pair(mp[u], i)] = 1;
			A_Edge(mp[u], i, inf);
			A_Edge(i, mp[u], 0);
		}
		mp[u] = i;
	}
	scanf("%d", &p);
	A_Edge(i, en, p);
	A_Edge(en, i, 0);
}

企鹅旅行

发现不容易用一个点处理每块浮冰的状态,于是想到拆点,将每块浮冰拆成两个点,入点和出点。入点处理跳入的企鹅,出点处理跳出的企鹅。因为每块浮冰最多能承受 m i m_i mi 次起跳,也就是说最多能有 m i m_i mi 只企鹅能跳出这块浮冰,那么我们就从出点连一条容量为 m i m_i mi 的边向入点。

每块浮冰站着的企鹅数就代表从源点连向此浮冰的边的容量,如果企鹅能从一块浮冰 u u u 跳到另一块 v v v 上,就从 u u u v v v 连一条容量为无线大的边,因为落地并不算在起跳次数中。

最后枚举每一块浮冰为汇点统计即可。

核心代码:

for (int i = 1; i <= n; i ++){
			en = i * 2, st = 0;
			mx_flow = 0;
			for (int j = 1; j <= n; j ++){
				A_Edge(j * 2, j * 2 - 1, sp[j]);
				A_Edge(j * 2 - 1, j * 2, 0);
				A_Edge(st, j * 2, su[j]);
				A_Edge(j * 2, st, 0);
			}
			for (int k = 1; k <= n; k ++){
				for (int j = 1; j <= n; j ++){
					if (k == j){
						continue;
					}
					if (Get_Dis(xp[k], yp[k], xp[j], yp[j]) <= d){
						A_Edge(k * 2 - 1, j * 2, inf);
						A_Edge(j * 2, k * 2 - 1, 0);
					}
				}
			}
			mx_flow = 0;
			while (1){
				int fl = bfs();
				if (fl){
					mx_flow += Dinic(st, inf);
					if (mx_flow == sum){
						ansp ++;
						printf("%d ", i - 1);
						break;
					}
				}
				else {
					break;
				}
			}
			memset(he, 0, sizeof(he));
			tot = 1;
		}
		if (ansp == 0){
			puts("-1");
			continue;
		}
		puts("");
	}

ZQC 的游戏

贪心地想,对于 1 号节点,他的重量肯定越大越好,所以他肯定要将范围内的铁球吃完。

对于剩下的人,因为要保证 1 号节点是重量最大的,那么他们能吃到最大重量就为 w e i g h t 1 − w e i g h t i weight_1 - weight_i weight1weighti,就从源点连一条这样的边。对于每一个他能吃到的食物,就连一条长度为无穷大的边表示他能吃到,最后连接食物和汇点的边,容量为食物重量限制。

注意此时跑最大流得到的就是剩下的人在保证重量小于 1 号节点时能吃到的最大值,题目要求的是要吃光所有能吃到的食物,判断两者的大小,如果最大值还不能满足将食物吃完,那么说明不能达到要求。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值