Description
Every time it rains on Farmer John's fields, a pond forms over Bessie's favorite clover patch. This means that the clover is covered by water for awhile and takes quite a long time to regrow. Thus, Farmer John has built a set of drainage ditches so that Bessie's clover patch is never covered in water. Instead, the water is drained to a nearby stream. Being an ace engineer, Farmer John has also installed regulators at the beginning of each ditch, so he can control at what rate water flows into that ditch.
Farmer John knows not only how many gallons of water each ditch can transport per minute but also the exact layout of the ditches, which feed out of the pond and into each other and stream in a potentially complex network.
Given all this information, determine the maximum rate at which water can be transported out of the pond and into the stream. For any given ditch, water flows in only one direction, but there might be a way that water can flow in a circle.Input
The input includes several cases. For each case, the first line contains two space-separated integers, N (0 <= N <= 200) and M (2 <= M <= 200). N is the number of ditches that Farmer John has dug. M is the number of intersections points for those ditches. Intersection 1 is the pond. Intersection point M is the stream. Each of the following N lines contains three integers, Si, Ei, and Ci. Si and Ei (1 <= Si, Ei <= M) designate the intersections between which this ditch flows. Water will flow through this ditch from Si to Ei. Ci (0 <= Ci <= 10,000,000) is the maximum rate at which water will flow through the ditch.Output
For each case, output a single integer, the maximum rate at which water may emptied from the pond.Sample Input
5 4 1 2 40 1 4 20 2 4 20 2 3 30 3 4 10Sample Output
50
思路
这道题目是关于网络流/最大流的裸题,已给定唯一源点、唯一汇点、点数、边数、边,拿来练手是最适合不过了。
涉及到较多的概念,比如流量、最大流、容量、残余容量、增广路、可行流、增广操作、反向边、取消流操作、残余网络、切割、最小切割、最大流最小切割定理...
个人觉得反向边是其中最重要的概念了,正向边的标号记录正向边的残余容量,反向边的标号记录正向边的流量,这样的好处是当流走错时,我们可以取消流,提供了“反悔”的可能
详细定义需要参考白书第11章图论模型与算法或者《算法导论》 第26章最大流的内容。
我觉得几个不错的教网络流的资源:浅析网络流原理[1]、EK算法[2]、Dinic算法[3]
AC代码如下:
EK算法,O(n * m^2) ,适合稀疏图
#include<iostream> #include<algorithm> #include<cstring> #include<vector> #include<queue> #include<cstdio> const int INF = 0x3f3f3f3f; const int MAXN = 210; //点 const int MAXM = 210; //边 int G[MAXN][MAXN], flow[MAXM], pre[MAXM]; int n, m; //BFS寻找增广路 //一次BFS进行一次增广 std::queue<int> q; int bfs (const int& s, const int& t) { while (!q.empty()) q.pop(); memset(pre, -1, sizeof(pre)); pre[s] = 0; flow[s] = INF; q.push(s); while(!q.empty()) { int p = q.front(); q.pop(); //已走到汇点 if (p == t) break; //搜索邻边(稀疏图可以这么干) for (int u = 1; u <= n; u++) { //u不是源点且弧p->u还有容量且点u未走过 if (u != s && G[p][u] > 0 && pre[u] == -1) { //更新增广路,加入残余网络 pre[u] = p; flow[u] = std::min(flow[p], G[p][u]); q.push(u); } } } //走不到汇点 if (pre[t] == -1) return -1; //否则返回增广路的流量 return flow[t]; } //EK求最大流 //时间复杂度O(n * m^2) ,适用于稀疏图 int EK (const int& s, const int& t) { int delta = 0; //新的增广路的流量,用于执行抵消操作,生成残余网络 int tot = 0; //被更新的最大流的值 while(1) { delta = bfs(s, t); //找不到增广路,由最大流最小切割定理知残余网络已生成最大流 if (delta == -1) break; //找到增广路,进行抵消操作 int p = t; //沿增广路的反方向抵消回去 while (p != s) { G[pre[p]][p] -= delta; //正向弧的容量要减去delta G[p][pre[p]] += delta; //反向弧的容量要加上delta p = pre[p]; } tot += delta; } return tot; } int main(void) { int u, v, w; while(~scanf("%d%d", &m, &n)) { memset(G, 0, sizeof(G)); memset(flow, 0, sizeof(flow)); for (int i = 0; i < m; i++) { scanf("%d%d%d", &u, &v, &w); G[u][v] += w; } printf("%d\n", EK(1, n)); } return 0; }
Dinic算法,O(n^2 * m),适合稠密图
#include<iostream> #include<algorithm> #include<queue> #include<cstdio> #include<cstring> const int INF = 0x3f3f3f3f; const int MAXN = 210; //点 const int MAXM = 50000; //边 int n, m; struct Edge{ int to, w, next; }e[MAXM]; int head[MAXN], cnt; //head存储以u为起点的最后一条边在e中的位置 void initG() { cnt = 0; memset(head, -1, sizeof(head)); } void addEdge (const int& u, const int& v, const int& w) { //同时加正向边与反向边 //可以利用异或的对偶性质实现正向边与反向边索引的快速查找 //如偶数 0 存正向边,其反向边的索引对应的是0^1 = 奇数1 //反向边查其正向边也成立 e[cnt].to = v; e[cnt].w = w; e[cnt].next = head[u]; head[u] = cnt++; e[cnt].to = u; e[cnt].w = 0; e[cnt].next = head[v]; head[v] = cnt++; } //BFS分层并判断是否存在增广路 int q[MAXN]; int d[MAXN]; //点的层数,也是点离源点的最小边数 bool bfs () { memset(d, -1, sizeof(d)); int front = 0, tail = 0; d[1] = 0; q[tail++] = 1; while(front < tail) { int u = q[front++]; if (u == n) return true; for (int i = head[u]; ~i; i = e[i].next) { const int& v = e[i].to, w = e[i].w; //更新v的层数并判断弧u->v的容量是否大于0 //只有容量为正,流量才可能通过,才能增广 if (d[v] == -1 && w) { d[v] = d[u] + 1; q[tail++] = v; } } } return false; //无法从源点通过分层走到汇点,则不存在增广路 } //dfs从前一层往后一层反复寻找增广路 //一次dfs,多次增广 int dfs (const int& s, const int& t, const int& flow) { if (s == t) return flow; //找到一条增广路径 int pre = 0; //之前的增广路消耗的总流量 //多路增广 for (int i = head[s]; ~i; i = e[i].next) { const int& v = e[i].to, w = e[i].w; if (d[s] + 1 == d[v] && w > 0) { int tmp = std::min(flow - pre, w); //tmp表示当前可行的流量 int tmpflow = dfs(v, t, tmp); e[i].w -= tmpflow; e[i^1].w += tmpflow; pre += tmpflow; if (pre == flow) return pre; } } return pre; } //Dinic 算法求最大流 //时间复杂度 O(n^2 * m) 适合稠密图 int dinic () { int ret = 0; //利用bfs建立分层关系并判断是否存在增广路 while(bfs()) ret += dfs(1, n, INF); return ret; } int main(void) { int u, v, w; while(~scanf("%d%d", &m, &n)) { initG(); for (int i = 0; i < m; i++) { scanf("%d%d%d", &u, &v, &w); addEdge(u, v, w); } printf("%d\n", dinic()); } return 0; }
额外问一个问题,比较有价值,如果想得到最大流中每条边的流量,该怎么办呢?
将原图备份并称为G2,做一次最大流得到最终的残余网络Gf,让G2的正向边的容量减去Gf正向边的残余容量,就得到了最大流中每条边的流量。
参考
[1] 最大流的原理图解: https://blog.csdn.net/lirewriter/article/details/78759337
[2] 蒟蒻算法讲堂——EK算法:https://www.bilibili.com/video/av18567992/?p=1
[3]蒟蒻算法讲堂——Dinic算法:https://www.bilibili.com/video/av18800207/?p=2