算法学习笔记 - 网络流初步

前言

网络流是图论中一个博大精深的分支。本篇博客只希望让大家建立对网络流的初步认识,内容会比较浅显。

一个网络 G = (V, E) 是一张有向图,图中每条有向边 (x, y)∈E 都有一个给定的权值 c(x, y),称为边的容量。特别地,若 (x, y) 不属于 E,则 c(x, y) = 0。图中还有两个指定的特殊节点 S∈V 和 T∈V(S≠T),分别称为源点和汇点

设 f(x, y) 是定义在节点二元组 (x∈V, y∈V) 上的实数函数,且满足:

1. f(x, y) ≤ c(x, y)

2. f(x, y) = - f(y, x)

3. 任意x≠S,x≠T, \sum_{(u, x)\in E}f(u, x)=\sum_{(x,v)\in E}f(x,v)

f 称为网络的流函数。对于 (x, y)∈ E,f(x, y) 称为边的流量,c(x, y) - f(x, y) 称为边的剩余容量。

\sum_{(S,v)\in E} f(S,v)称为整个网络的流量(其中 S 表示源点)。

如下图所示。左边是一个网络,有源点 S、汇点 T 以及 A~E 共 7 个点。有向边上的数值标识了边的容量,例如 c(A, B) = 5。

 右边画出了该网络的一个流,在有向边上用 “f(x, y)/c(x, y)” 的形式标注流量,例如 f(A, B) = 4。流量为 0 的边省略了标注。值得一提的是,按照流函数的定义,图中标注的每条边的反向边其实都有一个负的流量,例如 c(B, A) = 0,f(B, A) = -4,这些没有标出,请读者留意。 

上文给出的流函数 f(x, y) 的三条性质分别称为容量限制,斜对称和流量守恒。尤其是流量守恒定律,他告诉我们网络中除源点、汇点以外,任何节点不储存流,其流入总量等于流出总量。网络流模型可以形象地描述为:在不超过容量限制的前提下,“流”从源点源源不断地产生,流经整个网络,最终全部归于汇点。

最大流

洛谷【模板】网络最大流

对于一个给定的网络,合法的流函数 f 有很多。其中使得整个网络的流量\sum_{(S,v)\in E} f(S,v)(其中 S 表示源点)的流函数被称为网络的最大流,此时的流量被称为网络的最大流量。

最大流能解决许多实际的问题。就拿我们上两节讨论的二分图为例。对于一张 n 个点 m 条边的二分图,我们可以新增一个源点 S 和 一个汇点 T,从 S 到每个左部点连有向边,从每个右部点到 T 连有向边,把原二分图的每条边看作从左部点到右部点的有向边,形成一张 n+2 个点 n+m 条边的网络,网络中每条边的容量都设为 1,如下图所示。容易发现,二分图的最大匹配就等于网络的最大流量,求出最大流后,所有有“流”经过的点、边就是匹配点、匹配边。

进一步地,若要求二分图多重匹配,则只需把 S 到左部点的有向边容量设为左部点的匹配数量上限 kli,右部点到 T 的有向边容量设为右部点的匹配数量上限 kri,原二分图每条边的容量设为 1,再求最大流即可。

Edmonds - Karp 增广路算法

若一条从源点 S 到汇点 T 的路径上各条边的剩余容量都大于 0,则称这条路径为一条增广路。显然,可以让一股流沿着增广路从 S 流到 T,使网络的流量增大。 Edmonds - Karp 算法的思路就是不断用 BFS 寻找增广路,直至网络上不存在增广路为止。

在每轮寻找增广路的过程中,Edmonds - Karp 算法只考虑图中所有 f(x, y) <c(x, y) 的边,用 BFS 找到任意一条从 S 到 T 的路径,同时计算出路径上各边的剩余容量的最小值 minf,则网络的流量就可以增加 minf。

需要注意的是,当一条边的流量 f(x, y) > 0 时,根据斜对称性质,它的反向边流量 f(x, y) < 0,此时必定有 f(y, x) < c(y, x)。故 Edmonds - Karp 算法在 BFS 时除了原图的边集 E 之外,还应该考虑遍历 E 中每条边的反向边。

在具体实现时,我们可以按照邻接表“成对存储”技巧,把网络的每条有向边及其反向边存在邻接表下称为 “2 和 3” “4 和 5” “6 和 7”...的位置上,每条边只记录剩余容量 c - f 即可。当一条边 (x, y) 流过大小为 e 的流时,令 (x, y) 的剩余容量减小 e,(y, x) 的剩余容量增大 e。

如下图所示。第一幅图是网络中每条边的原始容量,Edmonds - Karp 算法找到了流量为 5 的增广路 S→C→D→B→T。于是在第二幅图中,剩余容量对应发生了变化。紧接着,第三幅图中,算法又找到了流量为 2 的增广路 S→A→B→D→E→T。最终在第四幅图中,网络不存在增广路,最大流的大小就是 7。图中省略了剩余容量为 0 的边。

Edmonds - Karp 增广路算法的时间复杂度 O(nm^2)。然而在实际运用中则远远达不到这个上界,效率较高,一般能够处理 10^3~10^4 规模的网络。

代码示例

/*
网络最大流
采用 Edmonds - Karp 增广路算法
*/

#include<iostream>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;

#define int long long

const int maxn = 1001000;
int n, m, s, t;
int tot;
int maxflow;
int head[maxn];
int vis[maxn];
int dis[maxn];
int flow[maxn];
int last[maxn];

queue<int> q;

struct edge{
    int to;
    int nxt;
    int from;
    int val;
}e[2*maxn];

void add(int x, int y, int z){
    tot++;
    e[tot].to = y;
    e[tot].from = x;
    e[tot].val = z;
    e[tot].nxt = head[x];
    head[x] = tot;
}

bool bfs(int S, int T){
    for(int i = 1; i <= n; i++) last[i] = vis[i] = -1;
    q.push(S);
    dis[S] = 0;
    vis[S] = 1;
    flow[S] = 0x3f3f3f3f; // 增广路上各边的最小剩余容量
    while(!q.empty()){
        int u = q.front();
        q.pop();
        vis[u] = 0;
        for(int i = head[u]; i; i = e[i].nxt){
            int v = e[i].to;
            if(vis[v] == -1 && e[i].val){
                flow[v] = min(flow[u], e[i].val);
                last[v] = i; // 记录前驱,便于找到最长路的实际方案
                q.push(v);
                vis[v] = 0;
            }
        }
    }
    if(vis[T] != -1) return 1;
    return 0;
}

void update(int S, int T){ // 更新增广路及其反向边剩余容量
    int now = T;
    while(now != S){
        int i = last[now];
        e[i].val -= flow[T];
        e[i ^ 1].val += flow[T]; // 利用“成对存储”的 xor 1 技巧
        now = e[i].from;
    }
    maxflow += flow[T];
}

signed main(){
    tot = 1;
    cin >> n >> m >> s >> t;
    for(int i = 1; i <= m; i++){
        int x, y, z;
        cin >> x >> y >> z;
        add(x, y, z);
        add(y, x, 0);
    }
    maxflow = 0;
    while(bfs(s, t)) update(s, t);
    cout << maxflow << endl;
    return 0;
}

Dinic 算法

在任意时刻,网络中所有节点以及剩余容量大于 0 的边构成的子图被称为残量网络。Edmonds - Karp 每轮可能会遍历整个残量网络,但只找出 1 条增广路,还有进一步优化的空间。

之前我们应该就了解过节点的层次 d[x],它表示 S 到 x 最少需要经过的边数。在残量网络中,满足 d[y] = d[x] + 1 的边 (x, y) 构成的子图被称为分层图。分层图显然是一张有向无环图。

Dinic 算法不断重复以下步骤,直到在残量网络中 S 不能到达 T:

1. 在残量网络上 BFS 求出节点的层次,构造分层图。

2. 在分层图上 DFS 寻找增广路,在回溯时实时更新剩余容量。另外,每个点可以流向多条出边,同时还加入了若干剪枝。

Dinic 算法的时间复杂度为 O(mn^2)。实际运用中远远达不到这个上界,可以说是比较容易实现的效率最高的网络流算法之一,一般能够处理 10^4~10^5 规模的网络。特别地,Dinic 算法求解二分图最大匹配的时间复杂度为 O(m\sqrt{n})

代码示例

/*
网络最大流
采用 Dinic 算法
*/

#include<iostream>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;

#define int long long

const int maxn = 1000100;
int n, m, s, t;
int tot;
int maxflow;
int head[maxn];
int dis[maxn];

queue<int> q;

struct edge{
    int from;
    int to;
    int nxt;
    int val;
}e[2*maxn];

void add(int x, int y, int z){
    tot++;
    e[tot].from = x;
    e[tot].to = y;
    e[tot].val = z;
    e[tot].nxt = head[x];
    head[x] = tot;
}

bool bfs(){ // 在残量网络上构造分层图
    memset(dis, 0, sizeof(dis));
    while(!q.empty()) q.pop();
    q.push(s);
    dis[s] = 1;
    while(!q.empty()){
        int x = q.front();
        q.pop();
        for(int i = head[x]; i; i = e[i].nxt){
            int y = e[i].to;
            if(!dis[y] && e[i].val){
                q.push(y);
                dis[y] = dis[x] + 1;
                if(y == t) return 1; 
            }
        }
    }
    return 0;
}

int dinic(int x, int flow){ // 在当前分层图上增广
    if(x == t) return flow;
    int rest = flow;
    int k;
    for(int i = head[x]; i && rest; i = e[i].nxt){
        int y = e[i].to;
        if(dis[y] == dis[x] + 1 && e[i].val){
            k = dinic(y, min(rest, e[i].val));
            if(!k) dis[y] = 0; // 剪枝,去掉增广完毕的点
            e[i].val -= k;
            e[i ^ 1].val += k;
            rest -= k;
        }
    }
    return flow - rest;
}

signed main(){
    cin >> n >> m >> s >> t;
    tot = 1;
    int x, y, z;
    for(int i = 1; i <= m; i++){
        cin >> x >> y >> z;
        add(x, y, z);
        add(y, x, 0);
    }
    int flow = 0;
    while(bfs()) while(flow = dinic(s, 0x3f3f3f)) maxflow += flow;
    cout << maxflow << endl;
    system("pause");
}

二分图最大匹配的必须边与可行边

给定一张二分图,其最大匹配的方案不一定是唯一的。若任何一个最大匹配方案的匹配边都包括 (x, y),则称 (x, y) 为二分图最大匹配的必须边。若 (x, y) 至少属于一个最大匹配的方案,则称 (x, y) 为二分图最大匹配的可行边。

先来考虑一种简化情况——二分图的最大匹配是一组完备匹配。我们先任意求出一组完备匹配的方案,此时左,右部所有的节点都是匹配点。

根据定义,(x, y) 是必须边,当且仅当以下两个条件都满足

1. (x, y) 当前是匹配边。

2. 删除边 (x, y) 后,不能找到另一条从 x 到 y 的增广路。

(x, y) 是可行边,当且仅当满足以下两个条件之一

1. (x, y) 当前是匹配边。

2. (x, y) 是非匹配边,设当前 x 与 v 匹配,y 与 u 匹配,连接边 (x, y) 后,节点 u,v 失去匹配。必须能找到一条从 x 到 y 的路径。

考虑必须边的条件,(x, y) 是匹配边,对应 G1 中有一条从 y 到 x 的有向边。在此前提下,若 x 到 y 还存在一条路径,则 x 和 y 互相可达,必处于同一个强连通分量中。如下图所示。, 和 y 在 G1 中互相可达,构成一个环,把环上的边的匹配状态取反,得到另一组完备匹配。

 因此,必须边的判定条件可改写为:(x, y) 当前是二分图 G 的匹配边,并且 x,y 两点在有向图 G1 中属于不同的强连通分量。

类似地,可行边的判定条件可改写为:(x, y) 当前是二分图 G 的匹配边,或者 x,y 两点在有向图G1 中属于同一个强连通分量。

在一般的二分图中(最大匹配不一定是完备匹配),可以用最大流计算出任意一组最大匹配。此时必须边的判定条件为:(x, y) 流量为 1,并且在残量网络上属于不同的强连通分量。可行边的判定条件为:(x, y) 流量为 1,或者在残量网络上属于同一个强连通分量。

最小割

给定一个网络 G = (V, E),源点为 S,汇点为 T。若一个边集 E'\subseteq E 被删去之后,源点 S 和汇点 T 不再连通,则称该边集为网络的。边的容量之和最小的割称为网络的最小割

最大流最小割定理

任何一个网络的最大流量等于最小割中边的容量之和,简记为 “最大流 = 最小割”。

假设 “最小割 < 最大流”,那么割去这些边之后,因为网络流量尚未最大化,所以仍然可以找到一条 S 到 T 的增广路,与 S,T 不连通矛盾,故 “最小割 ≥ 最大流”。如果我们能给出一个 “最小割 = 最大流”的构造方案,即可得到上述定理。

求出最大流后,从源点开始沿残量网络 BFS,标记能够到达的点。E 中所有连接 “已标记点 x” 和 “未标记点y” 的边 (x, y) 构成该网络的最小割。

费用流

给定一个网络 G = (V, E),每条边 (x, y) 除了有容量限制 c(x, y),还有一个给定的 “单位费用” w(x, y) 的流量为 f(x, y) 时,就要花费 f(x, y) * w(x, y)。该网络中总花费最小的最大流被称为 “最小费用最大流”,总花费最大的最大流被称为 “最大费用最大流”,二者合称为 “费用流” 模型。注意:费用流的前提是最大流,然后才考虑费用的最值。

Edmonds - Karp 增广路算法

在 Edmonds - Karp 求解最大流的基础上,把 “用 BFS 寻找任意一条增广路” 改为 “用 SPFA 寻找一条单位费用之和最小的增广路” (也就是把 w(x, y) 当作边权,在残量网络上求最短路) 即可求出最小费用最大流。注意:一条反向边 (y, x) 的费用应设为 - w(x, y)。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《Python学习笔记》是由皮大庆编写的一本关于Python语言学习的教材。在这本书中,作者详细介绍了Python语言的基础知识、语法规则以及常用的编程技巧。 首先,作者简要介绍了Python语言的特点和优势。他提到,Python是一种易于学习和使用的编程语言,受到了广大程序员的喜爱。Python具有简洁、清晰的语法结构,使得代码可读性极高,同时也提供了丰富的库和模块,能够快速实现各种功能。 接着,作者详细讲解了Python的基本语法。他从变量、数据类型、运算符等基础知识开始,逐步介绍了条件语句、循环控制、函数、模块等高级概念。同时,作者通过大量的示例代码和实践案例,帮助读者加深对Python编程的理解和应用。 在书中,作者还特别强调了编写规范和良好的编程习惯。他从命名规范、注释风格、代码缩进等方面指导读者如何写出清晰、可读性强的Python代码。作者认为,良好的编程习惯对于提高代码质量和提高工作效率非常重要。 此外,作者还介绍了Python的常用库和模块。他提到了一些常用的库,如Numpy、Pandas、Matplotlib等。这些库在数据处理、科学计算、可视化等领域有广泛的应用,帮助读者更好地解决实际问题。 总的来说,《Python学习笔记》是一本非常实用和全面的Python学习教材。通过学习这本书,读者可以系统地学习和掌握Python编程的基础知识和高级应用技巧,为以后的编程学习和工作打下坚实的基础。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值