网络流之最大流模型,看不懂打我!

最大流模型通常可以解决抽象出的具体的流网络求最大流的问题。

目录

1、初识模板

(1)EK算法

(2)Dinic算法

(3)ISAP算法

2、应用——二分图匹配

3、应用——上下界可行流

(1)、无源汇上下界可行流

(2)、有源汇上下界最大流

4、一些其他的最大流问题

(1)、多源汇最大流

(2)、关键边


1、初识模板

先上题目:

给定一个包含 n 个点 m 条边的有向图,并给定每条边的容量,边的容量非负。

图中可能存在重边和自环。求从点 S 到点 T 的最大流。

 不错,很经典的模板题。网络流的名词概念不再多言,这里介绍两种求最大流的算法:EK算法和Dinic算法

(1)EK算法

首先找一个可行流,找可行流的方法很简单,bfs一遍即可;

假设我们已经通过bfs找出一个可行流 f,

EK算法的原理即是下面的两两组合便可证明出第三个式子的命题

1、求出的可行流 f 是最大流

2、f 的残留网络中不存在增广路

3、f 的流量等于某个割的容量

证明:

1 \Rightarrow 2:易证,f 的残留网络若存在增广路,则 f 加上这条增广路后流量会变大,则与 f 是最大流矛盾,所以命题得证;

\Rightarrow 1:已知任何一个可行流的流量都不大于任何一个割的容量  | f | <= c(s, t)

                则最大流F也满足该不等式|F| <= c(s,t)

                而 |f| = c(s, t)

                所以|f| = |F|,f 为最大流,命题得证

这段推理证明了最大流 <= 最小割,同样的,最小割 c(s, t) = | f | <= | F | = 最大流,

也就是最小割 = 最大流,这就是最大流最小割定理

\Rightarrow 3 :对于2所找出的这条可行流(也就是残留网络没有增广路这条可行流),我们可以构造一个割:S中包含在残留网络上,从源点出发所能走到的所有点,T是S的补集,即T = U - S;

然后

| f | = f(s,t) = \sum_{u\in S } \sum_{v\in T} f(u,v) - \sum_{u'\in S } \sum_{v'\in T} f(v',u')= c(S,T)

//这里记得补充一下这个公式的说明

命题得证

那么这三条公式能够相互推演,这也就是EK算法的理论基础

上代码:

int EK() {
    int res = 0;
    while (bfs()) {
        res += d[T];
        for (int i = T; i != S; i = e[pre[i] ^ 1]) 
            fc[pre[i]] -= d[T], fc[pre[i] ^ 1] += d[T];
    }
    return res;
}

其实有了理论基础,那么解决问题就可以放开手脚干了,可以很轻松的看出,这段代码的意思就是找一条可行流——找出它的增广路——把它增广路的容量加进去得到一条新流——找出新的增广路如此循环下去,最后找不到增广路了,并且我们可以计算出这条可行流的流量,那么恭喜你,这就是最大流。

看上去好像第三条定理没有用上?其实隐含在循环之中了,并不需要单独拎出来证明,我们只需要知道这条理论存在,他可以帮助我们从2证明出1就可以了。3也可以说是最大流存在定理。

补充一下bfs求可行流

//bfs求可行流
bool bfs() {
    int hh = 0, tt = 0;
    memset(st, false, sizeof st);
    q[0] = S; st[S] = true; d[S] = INF;
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; ~i; i = ne[i]) {
            int ver = e[i];
            if (!st[ver] && fc[i]) {
                st[ver] = true;
                d[ver] = min(d[t], fc[i]);
                pre[ver] = i;
                if (ver == T) return true;
                q[++tt] = ver;
            }
        }
    }
    return false;
}

(2)Dinic算法

仔细观察EK算法我们可以发现EK每次都是从源点找,一直找到汇点,才找到一条增广路出来。如果增广路不巧每次都是上一个点连接的最后一个点,那么复杂度将会达到惊人的n^2级别,那么我们可不可以在找的时候一次性找到多条增广路,然后一起增广呢,Dinic就是从这个角度进行优化的。

想象一个物流站,它有若干需要配送的其他站点,我们可以把这个物流站出去的传送带进行编号,比方说1号线路的传送速度是15个物品/s,2号线路的传送速度是20个/s,一开始这个物流站是空的,现在有一条传送带正在源源不断地以10个物品每秒的速度向这个物流站补充资源,我们可以直接把它接到1号线上,速度是足够的。然后又有一个传送带同样以10个每秒的速度补充,我们在把1号线路填满之后,剩下的由二号线配送。那么接下来无论再来多少物品,我们都不用再管1号线了(因为它已经满了)对不对?现在第三个传送带以25个每秒的速度补给,但是我们只有两条出去的路,在把2号线路填满之后还是不能吞吐这么多的物品,那么我们直接拒绝这条线路的补给,告诉上一个物流站,你太多了,我屯不下!这样就不用每次都找一条从起点到终点的完整线路然后增加物流的配送,从而节省大量的搜索时间。

(注意看,这个加粗的地方就是当前弧优化,我们可以用cur数组存储还没满的是第几条线路,然后我们再配给任务的时候就从cur[当前物流站编号]开始)

但是我们该如何确定物流站的先后顺序呢?因为有自环的存在,我可以把当前的物品送给我们上级物流站从而做到左脚踩右脚升天的局面,搜索复杂度也居高不下,这里就要引入一个分层图的概念。

分层图:其实就是某个结点距离起点的距离,也就是BFS每一轮搜索的顺序。这样保证了我上一层搜完之后才会搜到下一层,物流的配给只能往同级或者下级传递。这样不但可以固定流入的总量,也能有效的避免自环的问题。

int Dinic() {
    int res = 0, flow;
    while (bfs()) while (flow = find(S, INF)) res += flow;
    return res;
}

这里的bfs的作用是建立分层图并看看是否可以找到增广路,每一次建立的分层图里的增广路全部建立完成后才会重新建立新的分层图,新的分层图将不再考虑已经满了的边,正如我们前面所说的那样。而find函数的作用相当于dfs,也就是从起点开始,按照分层图的顺序配送物品,在无法无法配给的时候及时中断。

bool bfs() {
    int hh = 0, tt = 0;
    memset(d, -1, sizeof d);
    q[0] = S; d[S] = 0; cur[S] = h[S];

    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; ~i; i = ne[i]) {
            int ver = e[i];
            if (d[ver] == -1 && fc[i]) {
                d[ver] = d[t] + 1;      //建立分层图
                cur[ver] = h[ver];      //初始化当前弧为第一条传送带
                if (ver == T) return true;
                q[++tt] = ver;
            }
        }
    }
    return false;
}

int find(int u, int limit) {
    if (u == T) return limit;
    int flow = 0;
    for (int i = cur[u]; ~i && flow < limit; i = ne[i]) {
        cur[u] = i;              //设置当前起始传送带(当前弧)
        int ver = e[i];
        if (d[ver] == d[u] + 1 && fc[i]) {
            int t = find(ver, min(fc[i], limit - flow));  //dfs接着向下搜索
            if (!t) d[ver] = -1;                         //爆仓了,拒绝配送
            fc[i] -= t; fc[i ^ 1] += t; flow += t;
        }
    }
    return flow;
}

 在你能够理解上述内容并且能自己独立写出dinic模板之后,再看一遍这个可视化视频,看与你理解的是否一致:敲了上百遍网络流算法,第一次看见它跑起来的样子

(3)ISAP算法

正在学习中。。。


2、应用——二分图匹配

我们之前学过匈牙利算法,它的时间复杂度为O(nm),其实二分图匹配的题目我们也可以通过建立流网络然后使用Dinic求解,这样在通常情况下速度会快很多。

还记得二分图匹配的概念吗,左边有一列点,右边也有一列点,左边的点可以与右边的若干点连接,连接的边有一个权值,现在要求最多能匹配多少对。在匈牙利算法中,我们采取的方案是现将左边的点依次与第一个能够匹配的点匹配,如果匹配的点已经被之前的点匹配了,就找是否存在一条增广路,使得该点能够匹配,否则就匹配下一个点。(我知道这段话看起来有点抽象,这里主要是讲网络流,不过多描述二分图的知识,后期我回头补前面基础的博客的时候会在这里放一个链接,这样能够直观详细地解释匈牙利算法。)

如果你熟知匈牙利算法的流程,你会发现,诶这不跟我们的dinic算法很像吗,都是先匹配第一个,然后找增广路的过程。那么其实我们优化的思路正是如此:首先把二分图建立成流网络,然后跑一遍dinic,最后求出的最大流即是二分图的最大匹配。

下面我们来证明一下为什么最大流和最大匹配相等价:

流网络需要源点和汇点,那么我们可以建立一个源点S与每一条左列的点连接,边权为1,代表如果走了这条路,相当于二分图中我选择了左边这个点,边权为1说明这个点在整个图中仅能出现一次,因为如果我选择这个点并将他匹配了,他就不会出现在其他匹配里。同样的,我们将右列的点都与汇点T连接,边权同理为1。那么中间的若干边就还是一样的边,如图所示: 

我们可以反证一下:假设我们求出的最大流不是最大匹配,意味着原图中必然存在增广路,使得我们左边或者右边可以连接更多的边,与最大流中不存在增广路的概念矛盾,所以我们求出的最大流 >= 最大匹配,反之亦然,最大匹配 >= 最大流,综上,最大流 = 最大匹配。

在明白这个公式之后,相关问题也就迎刃而解,不再多言。


3、应用——上下界可行流

(1)、无源汇上下界可行流

看例题:

给定一个包含 n 个点 m 条边的有向图,每条边都有一个流量下界low和流量上界up。

求一种可行方案使得在所有点满足流量平衡条件的前提下,所有边满足流量限制。

 也就是说每条边都要满足  Clow(u,v)\leqslant f(u, v)\leqslant Cup(u,v)

变型     0\leqslant f(u, v)-Clow(u,v) \leqslant Cup(u,v)-Clow(u,v)

每条边都不能低于low,则可以先让图中的所有边都 = low,很明显这样的话流量可能不守恒,所以我们要给每条边扩容形成一个可行流。

用公式表达即:下界图 + 扩容图 = 可行流  (核心思想)

在扩容图中,设A[ i ] 表示节点 i 为了达流量守恒所需要增加的流量(负数代表减少的流量)。

为了方便建图,我们设立一个超级源点S和超级汇点T。

此时我们可以分情况讨论:

1、如果出边比入边多(A[i]< 0 建立一条S\rightarrow i 边权为-A[i]的边来补上需要的流量;

2、如果入边比出边多(A[i]> 0建立一条i\rightarrow T 边权为A[i]的边来流走多余的流量;

3、每条边的边权:Clow(u,v)\leqslant f(u, v)\leqslant Cup(u,v) 

变型得    0\leqslant f(u, v)-Clow(u,v) \leqslant Cup(u,v)-Clow(u,v)

在扩容图里就是 up - low。

这样一来,差的补上了,多的流走了,整个图也就流量守恒了。

对于这样的一个新图,我们要求出它的最大流,并看最大流是否满足源点和汇点相邻的边为满流即为原题的可行流。

注意,最后要求原网络的可行流,那就应该拿扩容图 + 下界图,由于我们dinic存储的是残留网络,而残留网络的反向边与流量相等,所以应该拿反向边的值 + 边的下界得到确切的流量。

for (int i = 0; i < m * 2; i += 2) {
    cout << fc[i ^ 1] + low[i] << endl;
}

dinic模板代码略,主函数代码参考:

int main() {
    cin >> n >> m;
    S = 0, T = n + 1;
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; i++) {
        int a, b, c, d;
        cin >> a >> b >> c >> d;
        add(a, b, c, d);
        A[a] -= c; A[b] += c;
    }
    for (int i = 1; i <= n; i++) {
        if (A[i] > 0) add(S, i, 0, A[i]), tot += A[i];
        else if (A[i] < 0) add(i, T, 0, -A[i]);
    }
    if (dinic() != tot) cout << "NO";
    else {
        cout << "YES" << endl;
        for (int i = 0; i < m * 2; i += 2) {
            cout << fc[i ^ 1] + low[i] << endl;
        }
    }
    return 0;
}

(2)、有源汇上下界最大流

无源汇上下界可行流的思路是建立一个超级源点和汇点,如果原本就有源点汇点呢?

很简单,连一条从汇点到源点的容量为正无穷的边就行了,这样一来,源点和汇点也满足容量守恒,变成了无源汇图了。

那么求最大流呢?

一个很常见的误区就是在有超级源汇点的图上直接找增广路,找不到即为最大流,但其实不是。

有源汇上下界最大流 - AcWingAcWing,题解,有源汇上下界最大流,https://www.acwing.com/solution/content/41271/偷个懒放个博客,回头来补证明。


4、一些其他的最大流问题

(1)、多源汇最大流

给定一个包含 n 个点 m 条边的有向图,并给定每条边的容量,边的容量非负。

其中有 Sc 个源点,Tc 个汇点。

图中可能存在重边和自环。

求整个网络的最大流。

非常的简单,只需要建立虚拟超级源汇点ST,并向每一个源汇点连接一条容量无限制的边即可。不浪费篇幅写证明和代码。

(2)、关键边

对于一个流网络,如果对某条边进行扩容,可以让最大流变大,这条边就被称为关键边。

现在问给定的一个包含 n 个点 m 条边的有向图,并给定每条边的容量,边的容量非负,有一个源点S和一个汇点T,图中可能存在重边和自环。

求关键边的数量。

最大流的定义是残留网络中找不到增广路,如果想让最大流增大,就让增广路流通即可。

如果某一条边uv,能够从S走到u,并且能从v走到T,那么我们对这条边扩容,就可以获得一条增广路。

我们可以用dfs从起点搜一遍,再从重点反向搜一遍,把所有能搜到的点存储在bool数组里即可

dinic();
dfs(S, vis_s, 0);
dfs(T, vis_t, 1);

int cnt = 0;
for (int i = 0; i < m * 2; i += 2) 
    if (!fc[i] && vis_s[e[i ^ 1]] && vis_t[e[i]])
        cnt++;
cout << cnt << endl;

最后给上dfs的代码

void dfs(int u, bool st[], int k) {
    st[u] = true;
    for (int i = h[u]; ~i; i = ne[i]) {
        int ver = e[i];
        if (fc[i ^ k] && !st[ver])
            dfs(ver, st, k);
    }
}

记住一个小知识:原图的边 = 残留网络的反向边 = idx为偶数的边

(3)、最大流判定

最大流判定就是判断是否存在最大流以及最大是多少。具体到题目中就是将原图建立成一个流网络,以每一个可执行步骤当做一条边,在正确答案下最大流恰好可以满足题目限制。

看一道例题:

农夫约翰正在制造一台新的挤奶机,并希望对这件事进行严格保密。

他将挤奶机藏在了农场的深处,使他能够在不被发现的情况下,进行这项任务。

在机器制造的过程中,他要在牛棚和挤奶机之间一共进行 T 次往返。

他有一个秘密通道只能在返程时使用。

农场由 N 个地标(编号 1∼N)组成,这些地标由 P 条双向道路(编号 1∼P)连接。

每条道路的长度为正,且不超过 106。

多条道路可能连接同一对地标。

为了尽可能的减少自己被发现的可能性,农场中的任何一条道路都最多只能使用一次,并且他应该尝试使用尽可能短的道路。

帮助约翰从牛棚(地标 1)到达秘密挤奶机(地标 N)总共 TT 次。

找到他必须使用的最长的单个道路的最小可能长度。

请注意,目标是最小化所使用的最长道路的长度,而不是最小化所有被使用道路的长度之和。

保证约翰可以在不走重复道路的情况下完成 T 次行程。

想一想,应该怎么做?

绝对不是没时间更新了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值