网络
又称流网络 ( 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 u∈V ,e∈V,有实数函数 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=∑v∈Vf(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) (Ford–Fulkerson) 增广方法运用贪心的思想,通过寻找增广路来更新并求解最大流。
增广路:指 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) (Edmonds–Karp) 算法,是一种基于 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(v∗e2),其中 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;
}
例题
可以看出题目中有几个限制:
每个猪舍有定量的猪,顾客有最多能购买的猪头数。
顾客会根据顺序依次来到猪舍,顾客可以重新分配他能打开的猪舍中猪的头数。
所以考虑建立超级源点及汇点,以每一个顾客为节点,向汇点连他能购买的最多猪数。
因为每一个顾客如果不是第一次打开一个猪舍,那么就可以理解为他将吃上一个打开这个猪舍的顾客买剩下的,就将上一个顾客向此顾客连一条容量为无限大的边。如果是第一次打开,那么它将拥有购买的优先级,直接向源点连一条容量为此猪舍中猪头数的边。跑一个最大流即为答案。
核心代码:
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("");
}
贪心地想,对于 1 号节点,他的重量肯定越大越好,所以他肯定要将范围内的铁球吃完。
对于剩下的人,因为要保证 1 号节点是重量最大的,那么他们能吃到最大重量就为 w e i g h t 1 − w e i g h t i weight_1 - weight_i weight1−weighti,就从源点连一条这样的边。对于每一个他能吃到的食物,就连一条长度为无穷大的边表示他能吃到,最后连接食物和汇点的边,容量为食物重量限制。
注意此时跑最大流得到的就是剩下的人在保证重量小于 1 号节点时能吃到的最大值,题目要求的是要吃光所有能吃到的食物,判断两者的大小,如果最大值还不能满足将食物吃完,那么说明不能达到要求。