2019-07-28  2024-09-15    3343 字  7 分钟
  1. 最大流
  2. 最小费用最大流

最大流

引入

假设你所在的村庄开通了地下流水管道,自来水厂源源不断的提供水,村民们用水直接或间接用水,而村庄用完的废水统一回收于另一点(设从自来水厂流出的水全部回收)。当然每个管道有一定的容量,求出废水站最多可以汇聚多少水。

概念

**容量:**每条边都有一个容量(水管的最大水流容量) **源点:**出发点(水厂) **汇点:**结束点(废水站) **流:**一个合法解称作一个流,也就是一条可以从源点到汇点的一条合法路径。 **流量:**每条边各自被经过的次数称作其流量,最终收集的总数为整个流的流量。

限制

**容量限制:**每条边的流量不超过其容量(水管会爆炸) **流量平衡:**对于除源点和汇点以外的点来说,其流入量一定等于流出量。

解决

现在我们简化一下这个图,来解决这个问题。

x/y表示总流量为y,已经流了x

首先我们会想到找随即路径,然而如果走到如上图所示。

当走完 1->2->3->4 我们就找不到其他路径了,那么答案为1吗?不,答案为2.

那么现在我们改进算法,给流过的路径建立反向边

这样就给了一个反悔的机会。

定义一跳变得残量为:容量-已流过的流量

反向边的流量值=正向流过的总流量,也就是说正向流过多少,反向就可以流回多少。

从而我们又找到 1->3->2->4 的一条路径

再次建路径上的反向边,我们发现没有路径可以到达4点,所以答案为2.

小结

  1. 在图上找到一条从源点到汇点的路径(称为“增广路”)
  2. 去增广路上的残量最小值v(也就是流过的路径中流量最小的那一个)
  3. 将答案加上v
  4. 将增广路上所有边的残量减去v,反向边的残量加上v

重复上边四个步骤直到找不到增广路为止,这称作 FF 方法

首先这个算法必定不会死循环,因为每次增广都会导致流量增加(并且增加的是整数),而且流量有一个客观存在的最大值,所以它必定结束。

由于我们并没有指定它走那一条边,所以优先考虑随便走一条边。

考虑一种极限的情况:

现增广 1->2->3->4 会出现一条 3->2 容量为1的边。

再增广 1->3->2->4 ,再增广 1->2->3->4 …

浪费大量的时间,如果脸黑的话最多2e5次

然而我们如果先 1->2->4 然后 1->3->4 走两次就好了。

上面的做法是我们不期望的。我们可以考虑每次增广最短路 (EK算法)

EK算法

EK算法是以上算法的实现:每次寻找最短路进行增广。

时间复杂度 $_math_inline$O(m^2n)$math_inline_$

结构体储存三个变量 next to dis 【邻接表建边】

flow[i]:表示流过i点的v值,也就是说目前经过到i点的路径上的最小的残量。

dis[i]:表示i点距离源点的距离,S,T表示源点以及汇点

首先我们利用bfs处理图的连通性以及所有点于源点的距离,当然,当这条边上的残量已经为0的时候,我们认为它已经不能经过,我们可以直接不考虑。

在bfs中国呢pre数组是记录每个点最短路的前驱,last数组记录上条边的编号,从而记录出最短路径,然后从汇点进行更新即可。

Code
 1bool bfs(int s,int t) {
 2    memset(flow,0x7f,sizeof(flow));
 3    memset(dis,0x7f,sizeof(dis));
 4    memset(vis,0,sizeof(vis));
 5    Q.push(s);vis[s]=1;dis[s]=0,pre[t]=-1;
 6    
 7    while(!Q.empty()) {
 8        int temp=Q.front();
 9        Q.pop();
10        vis[temp]=0;
11        for(int i=head[temp];i!=-1;i=edge[i].nxt) {
12            int v=edge[i].to; 
13            
14            if(edge[i].flow>0&&dis[v]>dis[temp]+edge[i].dis) {
15                dis[v]=dis[temp]+edge[i].dis;
16                pre[v]=temp;
17                last[v]=i;
18                flow[v]=min(flow[temp],edge[i].flow);
19                if(!vis[v]) {
20                    vis[v]=1;
21                    Q.push(v); 
22                }
23            }
24        }
25    }
26    return pre[t]!=-1;
27}

从汇点向前更新

Code
 1while(bfs(s,t)) {
 2     int now=t;
 3     maxflow+=flow[t];
 4     mincost+=flow[t]*dis[t];
 5     while(now!=s) {
 6         edge[last[now]].flow-=flow[t];
 7         edge[last[now]^1].flow+=flow[t];
 8         now=pre[now];
 9     }
10 }

最大流最小割定理

什么是割?

比如:你的仇人是一个工厂老板。你要炸掉一些车,让他每个货物都运不到销售点。炸掉越大的车,你越容易被发现。你希望炸掉的车的容量之和尽量小,最小化这个值

选出一些边的集合,使得删除他们之后从源点无法到达汇点,那么这个集合就叫做一个割。这些边的容量之和叫做这个割的容量

**定理1:**任取一个割,其容量大于最大流的流量

从源点汇点每次都会经过割上的最少一条边。

割掉这条边以后,把源点能到达的边放在左边,不能到达的放在右边。

显然源点到汇点的流量不会超过从左边走向右边的次数,而这又不会超过从左边到右边的容量之和

**直观一点:**假设你是在车装着货物的时候炸掉了它。

每个货物你至少付出1的代价炸掉(流量小于容量的时候你要付出比货物数更多的代价),所以你炸的代价不会小于货物数

定理2: 最小割的容量大于最大流的流量,且FF方法能够正确的求出它。

这意味着一个惊人的事实:你能够仅付出和货物数相同的代价,就把你的仇人的财路炸断

考虑FF算法结束时,残量网络上没有了增广路

那么我们假设这个时候,从源点经过残量网络能达到的点组成的集合为X,不能达到的点为Y。显然汇点在Y例,并且残量网络上没有从X到Y的边。

可以发现以下事实成立:

  • Y到X的边流量为0,如果流量不为0,那么应该存在一条从X到Y的反向边,于是矛盾
  • X到Y的边流量等于其容量。只有这样它才不会在残量网络中出现

根据第一个条件,我们可以得知:没有流量从过年X到Y之后又回到X,所以当前流量应该等于X到Y的边的流量之和,根据第二个条件它又等于从X到Y的边容量之和,而所有从X到Y的边又构成一个割,其容量等于这些边的容量之和。

这意味着我们找到一个流和一个割,使得前者的流量等于后者的容量。

而根据前面的结论,最大流的流量不会超过这个割的容量,所以这个流一定是最大流。

荣养的,最小割的容量也不会小于这个流的流量,所以这个割也一定是最小割。

这就是FF法最后的局面(由于FF会终止,所以它必定求出这样一个局面),由此我们得出:FF是正确的,并且最大流等于最小割

EK优化-Dinic

EK时间复杂度太高,虽然大多数情况跑不到上界。

有一个显然的优化:

如果增广一次后发现最短路没有变化,那么可以继续增广,直到源点到汇点的增广路增大,才需要一遍bfs。

bfs之后我们除去那些可能在最短路上的边,即dis[终点]=dis[起点]+1的那些边

显然这些边构成的图中没有环,我们只需要沿这些边尽可能的增广即可

Code
 1int bfs() {
 2    memset(dis,-1,sizeof(dis));
 3    dis[S]=0;
 4    Q.push(S);
 5    while(!Q.empty()) {
 6        int u=Q.front();
 7        Q.pop() ;
 8        for(int i=head[u];i!=-1;i=edge[i].nxt) {
 9            int v=edge[i].to;
10            if(dis[v]==-1&&edge[i].w>0) {
11                dis[v]=dis[u]+1;    //更新 
12                Q.push(v); 
13            }
14        }
15    }
16    return dis[T]!=-1;    //判断是否联通。 
17}

当图连通时进行dfs,当前节点为u,每次经过与u距离最近的点,并且这条边的残量值要大于0,然后往后进行dfs。

我们在dfs时要加一个变量,作为流量控制(最后的流量不能超过前边流量的最小值)

dfs中变量flow记录这条管道之后的最大流量

Code
 1bool dfs(int u,int exp) {
 2    if(u==T)return exp;    //到达重点,全部接受。 
 3    int flow=0,tmp=0;    
 4    for(int i=head[u];i!=-1;i=edge[i].nxt) {
 5        int v=edge[i].to;    //下一个点。 
 6        if(dis[v]==dis[u]+1&&edge[i].w>0) {
 7            tmp=dfs(v,min(exp,edge[i].w));    //往下进行 
 8            if(!tmp)continue;
 9            
10            exp-=tmp;    //流量限制-流量,后边有判断。 
11            flow+=tmp;
12            
13            edge[i].w-=tmp;        //路径上的边残量减少 
14            edge[i^1].w+=tmp;    //流经的边的反向边残量增加。 
15            if(!exp)break;    //判断是否在限制边缘 
16        }
17    }
18    return flow;
19}

重复上边如果图连通(有最短路径),就一直增广

while(bfs())ans+=dfs(S,inf);

时间复杂度 $_math_inline$O(n^2m)$math_inline_$

在某些特殊情况下(每个点要么只有一条入边且容量为1,要么仅有一条出边且容量为1)其时间复杂度甚至能做到 $_math_inline$O(m\sqrt(n))$math_inline_$

Template

最大流模版 - 链式前向星存图
 1struct Flow_Dinic {
 2        Flow_Dinic(int n) {
 3            head = vector<int>(n + 10, -1);
 4            level = vector<int>(n + 10);
 5        }
 6
 7        struct edge {
 8            int to, cap, next;
 9        };
10
11        vector<int> head;
12        vector<int> level;
13
14        int s = 0, t = 0; // max_flow from s to t
15
16        vector<struct edge> edge;
17
18        void add_edge(int u, int v, int c) {
19            edge.push_back((struct edge) {v, c, head[u]});
20            head[u] = edge.size() - 1;
21            edge.push_back((struct edge) {u, 0, head[v]});
22            head[v] = edge.size() - 1;
23        }
24
25        bool bfs() {
26            fill(level.begin(), level.end(), 0);
27            level[s] = 1;
28            queue<int> que;
29            que.push(s);
30            while (!que.empty()) {
31                for (int i = head[que.front()]; ~i; i = edge[i].next) {
32                    if (edge[i].cap && !level[edge[i].to]) {
33                        level[edge[i].to] = level[que.front()] + 1;
34                        que.push(edge[i].to);
35                        if (edge[i].to == t) return true;
36                    }
37                }
38                que.pop();
39            }
40            return false;
41        }
42
43        int dfs(int f, int u) {
44            if (u == t) return f;
45            int d = 0, used = 0;
46            for (int i = head[u]; ~i; i = edge[i].next) {
47                if (edge[i].cap && level[u] == level[edge[i].to] - 1) {
48                    if ((d = dfs(min(f - used, edge[i].cap), edge[i].to))) {
49                        edge[i].cap -= d;
50                        edge[i ^ 1].cap += d;
51                        used += d;
52                    }
53                }
54            }
55            if (!used) level[u] = 0;
56            return used;
57        }
58
59        long long run(int _s, int _t) {
60            s = _s;
61            t = _t;
62            long long max_flow = 0;
63            while (bfs()) {
64                int d = 0;
65                while ((d = dfs(0x3f3f3f3f, s))) max_flow += d;
66            }
67            return max_flow;
68        }
69    };
最大流模版 - FAST - 邻接表存图
 1    struct Dinic {
 2        Dinic(int n) {
 3            G = vector<vector<int> >(n + 10);
 4            d = vector<int> (n+10);
 5            vis = vector<bool> (n+10);
 6            cur = vector<int> (n+10);
 7        }
 8
 9        struct Edge {
10            int from, to, cap, flow;
11        };
12
13        int s, t; //节点数,边数,源点编号,汇点编号
14        vector<Edge> edges; //边表,edges[e]和edges[e^1]互为反向弧
15        vector<vector<int> > G; //邻接表,G[i][j]表示节点i的第j条边在e中的序号
16        vector<bool> vis; //bfs用
17        vector<int> d; //从起点到i的距离
18        vector<int> cur; //当前弧下标
19        void add_edge(int from, int to, int cap) {
20            edges.push_back({from, to, cap, 0});
21            edges.push_back({to, from, 0, 0});
22            G[from].push_back(edges.size() - 2);
23            G[to].push_back(edges.size() - 1);
24        }
25
26        bool BFS() {
27            fill(vis.begin(), vis.end(), false);
28            queue<int> q;
29            q.push(s);
30            d[s] = 0;
31            vis[s] = true;
32            while (!q.empty()) {
33                for (int id : G[q.front()]) {
34                    Edge &e = edges[id];
35                    if (!vis[e.to] && e.cap > e.flow) {
36                        vis[e.to] = true;
37                        d[e.to] = d[q.front()] + 1;
38                        q.push(e.to);
39                    }
40                }
41                q.pop();
42            }
43            return vis[t];
44        }
45
46        long long DFS(int u, int a) {
47            if (u == t || a == 0) return a;
48            int flow = 0, f;
49            for (int &i = cur[u]; i < (int) G[u].size(); ++i) {
50                Edge &e = edges[G[u][i]];
51                if (d[u] + 1 == d[e.to] &&
52                    (f = DFS(e.to, min(a, e.cap - e.flow))) > 0) {
53                    e.flow += f;
54                    edges[G[u][i] ^ 1].flow -= f;
55                    flow += f;
56                    a -= f;
57                    if (a == 0) break;
58                }
59            }
60            return flow;
61        }
62
63        long long run(int _s, int _t) {
64            s = _s;
65            t = _t;
66            long long flow = 0;
67            while (BFS()) {
68                fill(cur.begin(), cur.end(), 0);
69                flow += DFS(s, 0x3f3f3f3f);
70            }
71            return flow;
72        }
73    };

最小费用最大流

除另有声明外本博客文章均采用 知识共享 (Creative Commons) 署名 4.0 国际许可协议 进行许可转载请注明原作者与文章出处