网络流

  • 网络流模型与最大流问题
  • Ford Fulkerson 框架
  • Edmonds-Kap 算法
  • Dinic 算法
  • MPM 多源点多汇点最大流算法
  • MCF 最小费用流
  • PushRelabel 压入与重标记算法



 


网络流模型与最大流问题

生活中的电网、水管网、交通运输网都有一个共同点:在网络传输中有方向和容量。

没错,给一张有向图增加一些限定,就是一个网络流模型。

8. 网络流_算法


限定:

  • 一个源点 s:入度为 0,所有流量的起点
  • 一个汇点 t:出度为 0,所有流量的终点
  • 容量限制:任何一条边的流量都不能大于容量
  • 流量限制:除了源点、汇点,其他节点,流入量 = 流出量

8. 网络流_最大流_02

网络流中最常见的问题就是网络最大流,即在满足容量限制、流量限制的条件下,从源点 t 最多能发出多少流量,这些流量可以存在整个网络中,最终汇聚到汇点 s。

 


Ford Fulkerson 框架

一个网络流模型:

8. 网络流_最大流_03


最大流路径:

8. 网络流_权值_04

要求出这样一条路径,需要引入反向边,把网络流模型变成残量图模型:

8. 网络流_最大流_05

  • 原图(网络流模型):节点 v -> 节点 w 的流量为 f,容量为 c
  • 新图(残量图模型):节点 v -> 节点 w 的权值为 c - f,节点 w -> 节点 v 的权值为 f

在残量图中,我们只要找到一条所有边的权值都大于 0 的路径:

  • 从源点 s 到汇点 t 的路径,沿途的边都大于 0

8. 网络流_网络流_06


这样一条路,就称为 — 增广路径。

这样的路径一定能注入新的流量。

Ford Fulkerson 思想:

  • 从原始图出发,创建残量图
  • 在残量图中不断寻找增广路径(源点->汇点,所有边的权值都大于 0 的路径)
  • 每找到一条增广路径,意味着流量增加了,这时候残量图需要更新权值
  • 再继续找增广路径,直到没有增广路径为止

Ford Fulkerson 思想在于,寻找增广路径,但没有说怎么寻找增广路径,所以找增广路径的方法不同,算法名字也就不同,但其实都是在找增广路径。

Ford Fulkerson 是一个算法框架,有不同的实现:

  • BFS(Edmond-Karp 算法)
  • DFS(Ford-Fulkerson 算法)
  • 先用 BFS 分层,再用 DFS 寻找(Dinic 算法)

搜索具体过程,请看 Edmond-Karp 算法。

Ford-Fulkerson 算法,代码实现:

#define INF 0xfffffff
// 无穷大
int n, m, s, t;      
// s是源点,t是汇点
bool vis[MAXN];

/* 链式前向星,使用数组模拟邻接表的方法 */
struct Edge {  
    int to;      // 目标顶点编号
    int w;       // 边的权值
    int next;    // 下一条边的编号  
} edges[MAXM];

int head[MAXM];  // head[i]表示从顶点i出发的第一条边在edge[]中的位置

/* 基本思想:在网络中寻找增广路,沿增广路增加流量,直到不存在增广路 */
int dfs_get_augmenting_path(int p, int flow) {
    if (p == t)                        // 到达终点
        return flow;                   // 返回这条增广路的流量
        
    vis[p] = true;
    for (int eg = head[p]; eg; eg = edges[eg].next) {
        int to = edges[eg].to, vol = edges[eg].w, c;
        // 返回的条件是残余容量大于0、未访问过该点且接下来可以达到终点(递归地实现)
        // 传递下去的流量是边的容量与当前流量中的较小值
        if (vol > 0 && !vis[to] && (c = dfs_get_augmenting_path(to, min(vol, flow))) != -1) {
            edges[eg].w -= c;
            edges[eg ^ 1].w += c;
            // 这是链式前向星取反向边的一种简易的方法
            // 建图时要把cnt置为1,且要保证反向边紧接着正向边建立
            return c;
        }
    }
    return -1; 
    // 无法到达终点
}

int Ford_Fulkerson() {
    int max_flow = 0, c;
    while ( (c = dfs_get_augmenting_path(s, INF)) != -1 ) {
        memset(vis, 0, sizeof(vis));
        max_flow += c;
    }
    return max_flow;
}

时间复杂度:8. 网络流_算法_078. 网络流_算法_08 边数、8. 网络流_权值_09

因为不断寻找增广路径,每寻找到一条,图的最大流都会增加 E,最多情况下需要找 8. 网络流_算法_10
 


Edmonds-Kap 算法

算法一开始,就把网络流模型变成残量图模型:

8. 网络流_最大流_11

Edmonds-Kap 就是用 BFS 寻找增广路径(源点->汇点,所有边的权值都大于 0 的路径)。

  • 在上图基础上,找到一条增广路径:0 -> 1 -> 3,并更新残量图权重:
  • 8. 网络流_算法_12

  • 在上图基础上,找到一条增广路径:0 -> 2 -> 3,并更新残量图权重:

8. 网络流_算法_13

  • 在上图基础上,找到一条增广路径:0 -> 1 -> 2 -> 3,并更新残量图权重:

8. 网络流_最大流_14


现在已经搜索不到增广路径,所以算法结束,当前流是最大流。

代码实现:

#define INF 0xfffffff
// 无穷大
int n, m, s, t, last[MAXN], flow[MAXN];
// s是源点,t是汇点

/* 链式前向星,使用数组模拟邻接表的方法 */
struct Edge {  
    int to;      // 目标顶点编号
    int w;       // 边的权值
    int next;    // 下一条边的编号  
} edges[MAXM];

int head[MAXM];  // head[i]表示从顶点i出发的第一条边在edge[]中的位置

int bfs_get_augmenting_path() {
    memset(last, -1, sizeof(last));
    queue<int> q;
    q.push(s);
    flow[s] = INF;
    while (!q.empty()) {
        int p = q.front();
        q.pop();
        if (p == t)                           // 到达汇点,结束搜索
            break;
        for (int eg = head[p]; eg; eg = edges[eg].next) {
            int to = edges[eg].to, vol = edges[eg].w;
            if (vol > 0 && last[to] == -1) {  // 如果残余容量大于0且未访问过(所以last保持在-1)
                last[to] = eg;
                flow[to] = min(flow[p], vol);
                q.push(to);
            }
        }
    }
    return last[t] != -1;
}

int Edmonds_Kap() {
    int maxflow = 0;
    while ( bfs_get_augmenting_path() ) {
        maxflow += flow[t];  // 最大流更新
        
        /* 从汇点原路返回更新残余容量 */
        for (int i = t; i != s; i = edges[last[i] ^ 1].to) {  
            edges[last[i]].w -= flow[t];
            edges[last[i] ^ 1].w += flow[t];
        }
    }
    return maxflow;
}

时间复杂度:8. 网络流_网络流_158. 网络流_算法_16 边数、8. 网络流_最大流_17

最多通过 8. 网络流_网络流_18 时间找不到增广路径了,而找一次增广路径需要 8. 网络流_算法_16,所以是 8. 网络流_网络流_20

 
Edmonds-Kap 算法,图论库(图论文章系列)实现:

  • 前置代码来源:adjMatrix * G 的 create_graph( *G, graph_kind )。
/* 深拷贝:保护数据,防止原数据因为当前算法被篡改 */
void deep_copy(adjMatrix *G, adjMatrix *nG){
    nG->n = G->n;
    nG->m = G->m;
    nG->kind = G->kind;
    nG->list = (T *) malloc(sizeof(T) * MAX_vertex);
    for(int i=0; i<G->n; i++)
        nG->list[i] = G->list[i];
    assert(nG->list != NULL);
    nG->matrix = (int **)malloc(sizeof(int) * MAX_vertex);
    for (int i = 0; i < MAX_vertex; i++) 
        nG->matrix[i] = (int *)malloc(sizeof(int) * MAX_edge);

    for(int i=0; i<G->n; i++)
        for(int  j=0; j<G->m; j++){
            nG->matrix[i][j] = G->matrix[i][j];
        }
}

/* 基本思想:在网络中寻找增广路,沿增广路增加流量,直到不存在增广路 */
bool get_augmenting_path(adjMatrix *nG, int s, int t, int *pre){
    bool visited[MAX_edge];
    memset(visited, false, sizeof(visited));

    std::queue <int> q; 
    q.push(s); 
    visited[s] = true; 
    pre[s] = -1; 

    while (!q.empty()) { 
        int j = q.front(); 
        q.pop(); 
  
        for (int i=0; i<nG->n; i++) { 
            if (visited[i]==false && nG->matrix[j][i] > 0) { 
                q.push(i); 
                pre[i] = j; 
                visited[i] = true; 
            } 
        } 
    }       
    return (visited[t] == true); 
}


int Edmonds_Kap(adjMatrix *G, adjMatrix *nG, int s, int t){
    deep_copy(G, nG);
    int max_flow = 0;
    int pre[MAX_edge];

    while( get_augmenting_path(nG, s, t, pre) ) {
        int path_flow = INT_MAX;
        
        /* 计算增广路径上的最小值 */
        for(int i=t; i!=s; i=pre[i]){
            int j = pre[i];
            path_flow = std::min(path_flow, nG->matrix[j][i]);
        }

		 max_flow += path_flow;            // 最大流更新
		 
		/* 根据增广路径更新残差图 */
        for(int i=t; i!=s; i=pre[i]) {
            int j = pre[i];
            nG->matrix[j][i] -= path_flow;
            nG->matrix[i][j] += path_flow;
        }
    }
    return max_flow;
}

 


Dinic 算法

最常用的网络流算法是 Dinic 算法。作为前俩者算法的优化,它选择了先用BFS分层,再用DFS寻找。

时间复杂度上界是 8. 网络流_网络流_21

所谓分层,其实就是预处理出源点到每个点的距离(注意每次循环都要预处理一次,因为有些边可能容量变为0不能再走)。我们只往层数高的方向增广,可以保证不走回头路也不绕圈子。

我们可以使用多路增广节省很多花在重复路线上的时间:在某点DFS找到一条增广路后,如果还剩下多余的流量未用,继续在该点DFS尝试找到更多增广路。

此外还有当前弧优化。因为一条边增广一次后,就不会再次增广了,所以下次增广时不需要再考虑这条边。

我们把 head 数组复制一份,但不断更新增广的起点。

/* 链式前向星,使用数组模拟邻接表的方法 */
struct Edge {  
    int to;      // 目标顶点编号
    int w;       // 边的权值
    int next;    // 下一条边的编号  
} edges[MAXM];

int head[MAXM];  // head[i]表示从顶点i出发的第一条边在edge[]中的位置

如果不使用 链式前向星,使用 vector

class Edge{
public:
    int u, v;
    T c;
    bool is_residual;
    Edge* counter_edge;                                     // 指向反向边
};

完整代码:

int n, m, s, t, lv[MAXN], cur[MAXN];                        // lv是每个点的层数,cur用于当前弧优化标记增广起点
inline bool bfs() {                                         // BFS分层
    memset(lv, -1, sizeof(lv));
    lv[s] = 0;
    memcpy(cur, head, sizeof(head));                        // 当前弧优化初始化
    queue<int> q;
    q.push(s);
    while (!q.empty()) {
        int p = q.front();
        q.pop();
        for (int eg = head[p]; eg; eg = edges[eg].next) {
            int to = edges[eg].to, vol = edges[eg].w;
            if (vol > 0 && lv[to] == -1)
                lv[to] = lv[p] + 1, q.push(to);
        }
    }
    return lv[t] != -1;                                     // 如果汇点未访问过说明已经无法达到汇点,此时返回false
}
int dfs(int p, int flow) {
    if (p == t)
        return flow;
    int rmn = flow;                                         // 剩余的流量
    for (int eg = cur[p]; eg && rmn; eg = edges[eg].next) { // 如果已经没有剩余流量则退出
        cur[p] = eg;                                        // 当前弧优化,更新当前弧
        int to = edges[eg].to, vol = edges[eg].w;
        if (vol > 0 && lv[to] == lv[p] + 1) {               // 往层数高的方向增广
            int c = dfs(to, min(vol, rmn));                 // 尽可能多地传递流量
            rmn -= c;                                       // 剩余流量减少
            edges[eg].w -= c;                               // 更新残余容量
            edges[eg ^ 1].w += c;                           // 链式前向星的cnt需要初始化为1(或-1)才能这样求反向边
        }
    }
    return flow - rmn;                                      // 返回传递出去的流量的大小
}

int dinic() {
    int ans = 0;
    while ( bfs() )
        ans += dfs(s, INF);
    return ans;
}

 


MPM 多源点多汇点最大流算法

 


MCF 最小费用流

 


PushRelabel 压入与重标记算法