最大流模板(一)BFS

Drainage Ditches
参考博客
用这篇博客学习了一下bfs求解最大流,然后先做一下笔记。(以下均为笔者自己理解,有不对之处还望大佬指出)
首先,我们学习最大流就要先了解什么是最大流。

最大流问题定义:管道网络中每条边的最大通过能力(容量)是有限的,实际流量不超过容量。最大流问题(maximum flow problem),一种组合最优化问题,就是要讨论如何充分利用装置的能力,使得运输的流量最大,以取得最好的效果。(摘自百度百科)
就是,我们给定一个图,这个图一般是一个有向图(下面讨论的都是有向图),其中的方向就是流向。其中还有一个源点和汇点。源点是图中不被任何节点指向的那个点,也就是可以想成这个源点向别的节点输出流,这个流的流量可以是无限大。汇点是不指向任何其他节点的点,由源点流出的最终汇集在这个节点。
那么一个图中由源点到汇点就会有很多条路径。我们要选择其中的有限条路径达到可以使得从源点到汇点的流量最大。
每两个节点之间有一条边(可以看做是一条通道,或是河流),因为每一段河流或说通道都有自己的运载能力(或说是容量),不可能通过无限的流,所以图中每一条边都有一个权值,这个权值代表的是这一条边的最大容量(即能通过的最大流量)。
最大流问题就是让我们选择网络流的路线,以达到从源点到汇点能通过的流(流量)最大。
这样就用到了我们对图的遍历算法——bfs。我们对一个图进行bfs遍历的时候,大多数情况是找到一条路径就退出了,所以我们需要一直bfs找路径,找到所有的可行路径,让所有路径并行(就是每一次bfs求得的最大流量加和)以使得整个图流量最大。
这里,我们找到路径以后,就在经过的边上减去这部分流量,从剩下的流量里继续找路径。可是有一个问题,我们这样找可能找的并不恰当。
因为可能原本有两条路径,但是在遍历过程中,其中一条路被另一条挤掉了,但这条路走其他路径也完全可以到达汇点(这是因为我们的遍历并不是智能的,毕竟计算机不会分析这样会不会挤掉另一条路),但这个时候我们这是后已经减掉了这部分流量。那么我们就反向加回来,当做是一种补偿,也可以是想成添加了反向流与一部分原来流抵消了。比如我们找到一条路径,那么会graph[u][v] -= min_flow;反向流就是加一个反向的同样大的流:graph[v][u] += min_flow;其实就是加了条反向边。

下面开始说一下这个反向流(这是我自己乱说的名词)的作用:
我理解的反向流的本质就是抵消错误流,等我解释完可能读者就能明白了。
首先,我们先假设,从源点到汇点只有两条相互独立(两条路径不相交)的路径(注意不是边)(用于解释这个反向流),

  • 如果这两条路径中间没有其他边连接的话,那么这样bfs就不会发生冲突了,所以就不用考虑了,直接不停bfs就行了。当然也就不用考虑反向流。
  • 如果这两条路径之间被一条边连接了(假设是从第一条路径上除源点和汇点的节点指向第二条路径中的某个点,读者可以自己在纸上画个草图,便于理解)。那么当我们在bfs过程中就有可能发生这样的情况:第一条路径同过这条边走到了第二条路径并且到达了汇点。那么问题来了,第二条路径的后半段被占用了,那么在遍历第二条路径的时候,我们走到与这个边的交点时,我们就走不下去了(如果没有反向流),因为后面被占了,联通两条路径的边也被占用了,那么这时候我们的程序就会认为已经找不到别的路径了,返回回来这个流量,那么显然是不对的。因为明明可以是两条路径的,却被挤占掉了。那么怎么办呢,这时候反向流就发挥作用了。我们在遍历第二条路径的时候走到交点,发现后面的那一段路不能走了,但是两条路径中间刚刚还增加了一条反向流,第二条路径便会沿着这个反向流走到第一条路径的后半段。发现,这条路后面还有容量可以承载流,那么就从这里走了。这样,虽然两条路径连通了,并且发生了交叉,但是并没有影响流的传输,不会存在被挤占路径的问题。和两条独立路径分别走是一样的效果。这样就将刚刚走错的流抵消掉了,相当于两条路径各走各的互不干扰。

(说到这里就差不多了,笔者语言叙述能力不强,又怕描述不清,所以就写了这么冗长的一段,如果还是没讲清楚,可以结合代码,可能就明白了,请见谅。)
下面就是代码实现了:

#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
const int maxn = 2e2 + 5, inf = 0x3f3f3f3f;
int graph[maxn][maxn];

int max_flow(int num, int source, int sink) //num代表图中节点个数,source代表源点,sink代表汇点
{
    int pre[maxn], min_flow[maxn];  //pre记录节点的父节点,min_flow记录某节点最多可以经过的流
    int ans = 0;
    while(true) //一直bfs直到找不到增广路径
    {
        queue<int> q;
        memset(pre, -1, sizeof(pre));   //父节点全都设为不存在
        pre[0] = -2;    min_flow[0] = inf;  //源点的父节点单独处理,假设我们从源头开始可以流出无限多
        q.push(source);
        while(q.size()) //bfs寻找增广路径
        {
            int temp = q.front();
            q.pop();

            for(int i = 1; i < num; i++)
            {
                if(!graph[temp][i] || pre[i] != -1) continue ;  //如果这个节点已经走过或者已经容量为0了,那么就跳过
                q.push(i);  //将可行子节点入队
                pre[i] = temp;  //记录i的父节点
                //比较父节点和子节点之间的剩余能通过的流和父节点能通过的流量,求最小,因为有一个地方通道小了,那么流量就被限制了
                min_flow[i] = min(graph[temp][i], min_flow[temp]);
            }
            //如果汇点有了父节点说明已经找到了一条路径,更改图中的流量返回,开始找一条新的路径
            if(pre[sink] != -1)
            {
                int son = sink;
                while(pre[son] >= 0)    //一直往前找并且更节点之间的最大通量
                {
                    //因为找到了一条路径,就需要在整个图的通量中减去这一条路径,
                    //或者理解成为这条路径预留出来这么多通量,然后从剩下的里面继续找路径
                    graph[pre[son]][son] -= min_flow[sink];
                    //这里给他一个后悔的机会,正向走一次,通量消耗了那么多,相当于加在了反向通量上
                    graph[son][pre[son]] += min_flow[sink];
                    son = pre[son];
                }
                break ;
            }//end of finding source
        }//end of bfs
        if(pre[sink] != -1) //说明bfs还能找到增广路径,那么将这条路径加入总答案,继续找
            ans += min_flow[sink];
        else    //否则说明找不到增广路径了,那么说明当前已经是最终答案了,返回
            return ans;
    }
}

int main()
{
    int m, n;    //n流通数,m为节点数量
    while(~scanf("%d %d", &n, &m))
    {
        int a, b, cost;
        memset(graph, 0, sizeof(graph));
        for(int i = 0; i < n; i++)
        {
			scanf("%d%d%d", &a, &b, &cost);
            graph[a-1][b-1] += cost;//网络
        }
        printf("%d\n", max_flow(m, 0, m-1));
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_40788897/article/details/84563505
今日推荐