最大流问题(超详细!!!)

0x00 最大流

最大流量问题涉及通过最大单源单汇流网络找到可行流量。如图所示。

每个边缘都标有容量,即可以携带的最大物品数量。 最大流问题是找出可以从顶点source到顶点sink总的物品数量(最大的那个)。

如上图所示,当0->12->1汇聚的时候不能超出1->3的值121的输出最大为12)。最后可以到达5的最大物品个数就是19+4=23。也就是每条边和每个顶点要满足:

  • 每条边上的流量不超过每条边的给定容量。
  • sourcesink之外,每个顶点的流入流等于流出流。

0x01 残差网络和增广路径

对于解决最大流的问题,我们首先将问题简单化,如图所示

首先可以想到的思路就是每次流过的值最大就好啦。所以,从S出发每次选择可以流出的最大流。

通过这种方式的得到的最后结果是3,显然这是错误的(应该是5)。我们首先将上面的结果中被删除的部分拿出来(没有被使用的流)。

通过该删除部分可以构成残差网络。给定图形 G G G和其中的流 f f f,我们形成一个新的流网络 G f G_f Gf,该网络具有相同的顶点集 G G G,并且每个 G G G边缘具有两个边。对于图 G G G的边 e = ( 1 , 2 ) e =(1,2) e=(1,2) ,对应流为 f ( e ) f(e) f(e),容量为 c ( e ) c(e) c(e),形成的 G f G_f Gf的方向为1->2的边(前向弧)容量为 c ( e ) − f ( e ) c(e)-f(e) c(e)f(e),方向为2->1的边(后向弧)的容量为 f ( e ) f(e) f(e)。 残差网络如图所示:

参差网络的思想对于解决最大流问题非常有用。

还有一个需要用到的概念是增广路径。所谓增广路径,是指这条路径上的流可以修改,通过修改,使得整个网络的流值增大。 设 f f f是一个可行流, P P P是从源点 s s s到终点(汇点) t t t的一条路,若 p p p满足下列条件:

  • p p p上的所有前向弧 ( v i → v j ) (v_i→v_j) (vivj)都是非饱和弧,即 0 ≤ f i , j < c i , j 0≤f_{i,j}<c_{i,j} 0fi,j<ci,j
  • p p p上的所有后向弧 ( v i ← v j ) (v_i←v_j) (vivj)都是非零弧,即 0 < f i , j ≤ c i , j 0<f_{i,j}≤c_{i,j} 0<fi,jci,j

则称 p p p为(关于可行流 f f f的)一条可增广路径。

0x02 Ford-Fulkerson算法

首先需要知道最大流最小割定理。如果 f f f是具有源点 s s s和汇点 t t t的流网络 G = ( V , E ) G = (V, E) G=(V,E)中的一个流,则下列条件是等价的:

  • f f f G G G的一个最大流。
  • 残留网络 G f G_f Gf不包含增广路径。
  • G G G的某个割 ( S , T ) (S, T) (S,T),有 ∣ f ∣ = c ( S , T ) |f| = c(S, T) f=c(S,T)

上述定理的证明可以参看这里定理证明,这里就不细说。

Ford-Fulkerson算法是一种迭代方法。开始时,对所有 u , v ∈ V u, v ∈ V u,vV f ( u , v ) = 0 f(u, v) = 0 f(u,v)=0,即初始状态时流的值为 0 0 0。在每次迭代中,可通过寻找一条增广路径来增加流值。增广路径可以看做是从源点 s s s到汇点 t t t之间的一条路径,沿该路径可以压入更多的流,从而增加流的值。反复进行这一过程,直至增广路径都被找出为止。最大流最小割定理将说明在算法终止时,这一过程可产生出最大流。

from collections import defaultdict 

class Graph: 
    def __init__(self,graph): 
        self.graph = graph 
        self.ROW = len(graph) 

    def bfs(self, s, t, parent):
        visited = [False]*self.ROW
        queue = [s] 
        visited[s] = True

        while queue: 
            u = queue.pop(0) 
            for ind, val in enumerate(self.graph[u]): 
                if visited[ind] == False and val > 0 : 
                    queue.append(ind) 
                    visited[ind] = True
                    parent[ind] = u 
        return True if visited[t] else False

    def FordFulkerson(self, source, sink): 
        parent = [-1]*self.ROW 
  
        max_flow = 0 
        while self.bfs(source, sink, parent): #判断增广路径 
            path_flow, s = float("Inf"), sink
            while s != source: 
                #计算增广路径的最小流量,通过最小流量计算残差网络
                path_flow = min(path_flow, self.graph[parent[s]][s]) 
                s = parent[s] 
                
            max_flow += path_flow 
            v = sink 
            while v != source: #计算残差网络
                u = parent[v] 
                self.graph[u][v] -= path_flow 
                self.graph[v][u] += path_flow 
                v = parent[v] 
        return max_flow 
  
graph = [[0, 16, 13, 0,  0,  0], 
         [0, 0,  10, 12, 0,  0], 
         [0, 4,  0,  0,  14, 0], 
         [0, 0,  9,  0,  0, 20], 
         [0, 0,  0,  7,  0,  4], 
         [0, 0,  0,  0,  0,  0]] 
  
g = Graph(graph) 
source, sink = 0, 5
print("The maximum possible flow is %d " % g.FordFulkerson(source, sink)) 

该算法的时间复杂度是 O ( V E 2 ) O(VE^2) O(VE2)

0x03 Dinic算法

Ford-Fulkerson算法对于稀疏图来说还不错,但是当边的数目过多的时候我们就需要更好的Dinic算法,该算法的时间复杂度是 O ( E V 2 ) O(EV^2) O(EV2)

首先使用给定的图初始化残差网络(和之前做法一样),如图所示。

第一次迭代:我们使用BFS为所有节点分配级别,同事需要检查是否有增广路径(残差网络中是否存在 s → t s→t st路径)。

现在,我们使用级别来发送流(意味着每个流路径的级别都应为0123)。与Ford-Fulkerson算法相比,我们一次发送三个流。

  • 路径 s – 1 – 3 – t s – 1 – 3 – t s13t上有4个流量单位。
  • 路径 s – 1 – 4 – t s – 1 – 4 – t s14t上有6个流量单位。
  • 路径 s – 2 – 4 – t s – 2 – 4 – t s24t上有4个流量单位。

总流量=4 + 6 + 4 = 14。一轮迭代后,残差图变为下图。

第二次迭代:我们使用bfs遍历上述修改后的残差图,为所有节点分配新级别。

现在,我们使用级别来发送流(意味着每个流路径的级别都应为01234)。 这次我们只能发送一个流。

  • 路径 s – 2 – 4 – 3 – t s – 2 – 4 – 3 – t s243t上有5个流量单位

总流量=总流量+ 5 = 19。二轮迭代后,残差图变为下图。

第二次迭代:此时已经没有增广路径了,所以结束。

#include <iostream>
#include <cstring>
using namespace std;

const int N = 1e5 + 10, M = N * 2;
struct Edge {
    int to, next, w;
} edge[M];

int cur[N], head[N], idx = -1; //cur用于当前弧优化
int n, m, s, t;

void add(int u, int v, int w)
{
    edge[++idx].to = v;
    edge[idx].next = head[u];
    edge[idx].w = w;
    head[u] = idx;
}

int q[N], level[N];
bool bfs(int u, int v) //判断是否存在增广路径,构建路径级别
{
    int hh = 0, tt = 0;
    q[0] = u;
    memset(level, -1, sizeof level);
    level[u] = 0;
    while (hh <= tt)
    {
        int tmp = q[hh++];
        for (int i = head[tmp]; ~i; i = edge[i].next)
        {
            int son = edge[i].to;
            if (edge[i].w <= 0 || ~level[son]) continue;
            level[son] = level[tmp] + 1;
            q[++tt] = son;
        }
    }
    return ~level[v];
}

int dfs(int u, int v, int flow) //寻找增广路径
{
    if (u == v) return flow;
    int res = 0;
    //注意使用引用,这样i变化的同时也能改变cur[u]的值,达到记录当前弧的目的
    for (int& i = cur[u]; ~i; i = edge[i].next)
    {
        int son = edge[i].to;
        if (edge[i].w <= 0 || level[son] != level[u] + 1) continue;
        if (res = dfs(son, v, min(edge[i].w, flow)))
        {
            edge[i].w -= res; edge[i^1].w += res;
            return res;
        }
    }
    return res;
}

int dinic(int u, int v)
{
    int res = 0;
    while (bfs(u, v))
    {
        for (int i = 0; i <= n; i++) cur[i] = head[i];
        res += dfs(u, v, 0x3f3f3f3f);
    }
    return res;
}

int main()
{
    cin >> n >> m >> s >> t;
    memset(head, -1, sizeof head);
    for (int i = 0; i < m; ++i)
    {
        int a, b, w;
        cin >> a >> b >> w;
        add(a, b, w), add(b, a, 0);
    }
    cout << dinic(s, t);
    return 0;
}

reference:

https://www.geeksforgeeks.org/max-flow-problem-introduction/

https://www.geeksforgeeks.org/ford-fulkerson-algorithm-for-maximum-flow-problem/

https://www.geeksforgeeks.org/dinics-algorithm-maximum-flow/

https://www.cnblogs.com/gaochundong/p/ford_fulkerson_maximum_flow_algorithm.html

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值