网络最大流是指在一个网络流图中可以从源点流到汇点的最大的流量。求解网络最大流的常用算法可以分为增广路径算法和预推进算法。其中,预推进算法的理论复杂度优于增广路径算法,但是编码复杂度过高,且效率优势在很多时候并不是很明显,因此,经常使用的算法为增广路径算法。
增广路径算法主要有Fold-Fulkerson算法,Edmonds-Karp算法,Dinic算法,ISAP算法。其中,Fold-Fulkerson 是最基本的增广路径思想,不能算作严格的算法实现。
增广路径
增广路径算法的思想是每次从源点出发找到一条到达汇点的可行路径,那么从源点到汇点的网络流至少可以增加w(w为这条路径上的边的最小容量)。此时,将最大流增加w,这条路径称为增广路径,同时从源到汇沿着增广路径将经过的每条正向边(从源指向汇)的流量都减去w,并将每条边的反向边的流量加上w。这个操作就为增广操作。
不断的进行增广操作,直到无法从源到达汇停止。那么,此时得到最大流的流量。同时,可以得到在获得最大流的时候,每条边上的流量分布(只需要将原图中每条边的容量减去最后的残余网络
中每条边对应的流量即可)。
残余网络
在增广路径的过程中每次进行增广操作之后,得到的新图称为旧图的残余网络。
1. Fold-Fulkerson算法
Fold-Fulkerson算法就是朴素的增广路径思想。
求最大流的过程,就是不断找到一条从源到汇的路径,然后构造残余网络,再在残余网络的基础上寻找新的路径,使总流量增加,然后形成新的残余网络,在寻找新路径.... 直到某个残余网络上找不到从源到汇的路径为止。
每用DFS执行一次找路径,增广的操作,都会使得最大流增加,假设最大流为C,那么时间复杂度可以达到 C*(m+n), m为边的数目,n为顶点的数目。
2. Edmonds-Karp算法
Edmonds-Karp算法是在Fold-Fulkerson思想上进行改进:
每次寻找增广路径的时候,总是寻找一条从源到汇经过节点数目最少的路径,即最短路径。这是一种“最短增广路径” Shortest Augmenting Path(SAP)的算法。
在实现的时候,每次利用BFS搜索,找到一条从源到汇的最短路径,然后进行增广操作;再进行BFS....直到无法找到从源到汇的路径为止。
时间复杂度可以达到 n*m*m, n为顶点的数目,m为边的数目。
EdmondsKarp算法的实现
//增广操作 int Augment(int s, int t, int n){ memset(gVisited, false, sizeof(gVisited)); gVisited[s] = true; int find_path = false; queue<int>Q; Q.push(s); while (!Q.empty()){ int u = Q.front(); Q.pop(); //广度优先搜索一条从源点到汇点的路径 for (int i = 0; i < n; i++){ if (gGraph[u][i] > 0 && !gVisited[i]){ gVisited[i] = true; gPre[i] = u; if (i == t){ find_path = true; break; } Q.push(i); } } } if (!find_path) return 0; int min_flow = 1 << 28; int u = t; //寻找路径上最小的容量 while (u != s){ int v = gPre[u]; min_flow = min_flow < gGraph[v][u] ? min_flow : gGraph[v][u]; u = v; } u = t; //进行增广操作 while (u != s){ int v = gPre[u]; gGraph[v][u] -= min_flow; gGraph[u][v] += min_flow; u = v; } return min_flow; }
int main(){
//buildgraph
int aug;
int max_flow = 0;
while (aug = Augment(s, t, n)){
max_flow += aug;
}
return 0;
}
3. Dinic算法
Edmonds-Karp算法每找到一条增广路径进行增广操作之后都再次回到原点重新进行BFS,这样效率较低。Dinic算法是在找到一条增广路径,增广操作完之后,并不回溯到源点,而是回到距离源点最近的边上流量为0的边的起点。因为每次增广操作,都会使得增广路径上的某些边的流量变为0,这样这条增广路径上的某些边就无法再走,需要回溯,但是回溯并不需要回溯到源点,只需要回溯到一点A,使得从源点到点A的路径上的流量不为0,且点A尽可能靠近汇点(为了遍历所有情况)即可。
具体的算法流程是:
1. 先用BFS对网络进行分层,分层是指按照距离源点的最近距离大小对各个点进行标号。
2. 然后利用DFS从前一层向后一层反复 每次选择增广路径的时候,从点u总是选择满足关系 dist[v] = dist[u] + 1的点v,这样u->v的路径肯定属于某条最短路。
3. 找到一条增广路径之后进行增广操作
4.路增广操作之后进行回溯,将点u回溯到点A,使得从源点到点A的路径上流量不为0,且点A尽可能靠近汇点径5. 从点u开始继续选择可行点v(满足 dist[v] = dist[u] + 1),直到汇点,这样就又找到一条增广路径....
6. 直到点u回溯到源点,再回到1继续操作,直到在分层操作时,无法用BFS找到从源到汇的路径。
Dinic 算法的实现
bool gVisited[N]; int gLayer[N]; int gGraph[N][N]; //将节点进行分层 bool CountLayer(int s, int t, int n){ deque<int> dQ; memset(gLayer, 0xFF, sizeof(gLayer)); dQ.push_back(s); gLayer[s] = 0; while (!dQ.empty()){ int v = dQ.front(); dQ.pop_front(); for (int i = 0; i < n; i++){ if (gLayer[i] == -1 && gGraph[v][i] > 0){ gLayer[i] = gLayer[v] + 1; if (i == t) return true; dQ.push_back(i); } } } return false; } int Dinic(int s, int t, int n){ int max_flow = 0; deque<int> dQ; while (CountLayer(s, t, n)){ dQ.push_back(s); memset(gVisited, false, sizeof(gVisited)); gVisited[s] = true; while (!dQ.empty()){ int v = dQ.front(); dQ.pop_front(); if (v == t){ int min_flow = 1 << 29; int min_vs = 0; for (int i = 0; i < dQ.size() - 1; i++){ int vs = dQ[i]; int ve = dQ[i + 1]; if (min_flow > gGraph[vs][ve]){ min_flow = gGraph[vs][ve]; min_vs = vs; } } //增广路径 max_flow += min_flow; for (int i = 0; i < dQ.size() - 1; i++){ int vs = dQ[i]; int ve = dQ[i + 1]; gGraph[vs][ve] -= min_flow; gGraph[ve][vs] += min_flow; } //回找到 min_flow的层次最小的节点位置 while (!dQ.empty() && dQ.back() != min_vs){ gVisited[dQ.back()] = false; dQ.pop_back(); } } else{ int i = 0; for (i = 0; i < n; i++){ if (!gVisited[i] && gGraph[v][i] > 0){ gVisited[i] = true; dQ.push_back(i); break; //找到一条可以继续走的路,就继续往下走。因为使用栈,所以break, } } if (i == n) //如果在这一层找不到往下走的路,进行回溯 dQ.pop_back(); } } } return max_flow; }
4. ISAP算法
ISAP算法为优化的最短增广路径算法(Improved Shortest Augmenting Path)。相比Dinic,ISAP算法不需要在回溯到源点之后再次进行BFS分层操作,而是在DFS以及回溯的过程中就进行了节点的重标号(即分层操作);以及ISAP算法进行gap优化大大提升效率。
具体流程为:
1. 定义dist[v] 为点v到达汇点的距离(即经过几个点到达汇点),定义gap[d]在当前残余网络中到达汇点(经过路径上流量不能为0)距离为d的点的个数。
2. 从汇点到源点进行BFS,标记下每个节点到达汇点的最短距离,即和Dinic算法相反的分层操作。
3. 当前点u从源点出发,用DFS找到一条到达汇点的路径....
4. 若点u为汇点,则找到了一条增广路径,进行增广操作;若点u可以向前走到v,且u-->v为一条可行边(dist[u] = dist[v]+1,且边u-->v流量不为0),则u走到v;若u无法向前推进到任何点,则对u进行重标号,然后回溯u到原来的增广路径中上一个点 pre[u].
5. 在重标号和回溯之前
,可以进行gap优化。gap[d]在当前残余网络中到达汇点(经过路径上流量不能为0)距离为d的点的个数。
若从u无法找到一条可行边,则表明可以经过 dist[u] 条边到达汇点的点数目少了一个,即 gap[dist[u]] --。若此时 gap[dist[u]] = 0,则说明当前残余网络中,没有任何一个点可以经过dist[u]条边到达汇点,源点到汇点的距离肯定大于等于dist[u],若源点能够到达汇点,那么要求源点到汇点的路径中肯定有到达汇点距离为dist[u]的点,所以,无法从源点到达汇点,此时,可以直接返回结果。
6. 重标号,是将点u重新分层,重新设置点u经过不为0的边可达汇点的最短距离。具体是 dist[u] = min{dist[v]|u连接到v,且u-->v边流量不为0} + 1. 若从u出发的边流量均为0,则无法找到下一个点,则直接将dist[u]置为n(n为节点个数),这样就说明u点不可达。
ISAP算法的实现(c++)
#include<stdio.h> #include<string.h> #include<iostream> #include<vector> #include<queue> #include<map> #include<algorithm> using namespace std; #define INFINITE 1 << 28 #define MAX_NODE 205 #define MAX_EDGE_NUM 500 #define min(a,b) a < b?a:b struct Edge{ int from; //起点 int to; //终点 int w; int next; //从from 出发的,下一条边的序号 int rev; //该边的反向边 序号 //用于查找反向边 bool operator == (const pair<int,int>& p){ return p.first == from && p.second == to; } }; Edge gEdges[MAX_EDGE_NUM]; int gHead[MAX_NODE]; int gPre[MAX_NODE]; int gPath[MAX_NODE]; int gGap[MAX_NODE]; int gDist[MAX_NODE]; int gFlow[MAX_NODE][MAX_NODE]; int gSource, gDestination; int gEdgeCount; void InsertEdge(int u, int v, int w){ Edge* it = find(gEdges, gEdges + gEdgeCount, pair<int, int>(u, v)); if (it != gEdges + gEdgeCount){ //如果已经有边 u --> v,则之前肯定已经指定了 u-->v 和 v-->u的反向关系 it->w += w; } else{ //添加 u-->v的边和反向边 v --> u int e1 = gEdgeCount; gEdges[e1].from = u; gEdges[e1].to = v; gEdges[e1].w = w; gEdges[e1].next = gHead[u]; gHead[u] = e1; gEdgeCount++; int e2 = gEdgeCount; gEdges[e2].from = v; gEdges[e2].to = u; gEdges[e2].w = 0; gEdges[e2].next = gHead[v]; gHead[v] = e2; //指定各个边的反向边 gEdges[e1].rev = e2; gEdges[e2].rev = e1; gEdgeCount++; } gFlow[u][v] = w; } //使用bfs进行分层,标记每个点到终点的距离 void Bfs(){ memset(gGap, 0, sizeof(gGap)); memset(gDist, -1, sizeof(gDist)); queue<int> Q; gGap[0] = 1; gDist[gDestination] = 0; Q.push(gDestination); while (!Q.empty()){ int n = Q.front(); Q.pop(); for (int e = gHead[n]; e != -1; e = gEdges[e].next){ int v = gEdges[e].to; if (gDist[v] >= 0){ //gDist初始值为-1. 如果>= 0,说明之前已经被访问过了 continue; } gDist[v] = gDist[n] + 1; gGap[gDist[v]] ++; Q.push(v); } } } int ISPA(int n){ //n 为顶点的个数 int ans = 0, u = gSource, d, e; while (gDist[gSource] <= n){ if (u == gDestination){//进行增广 int min_flow = INFINITE; for (e = gPath[u]; u != gSource; e = gPath[u = gPre[u]]) //找到路径中最小的边流量 min_flow = min(min_flow, gEdges[e].w); for (e = gPath[u = gDestination]; u != gSource; e = gPath[u = gPre[u]]){ //增广操作 gEdges[e].w -= min_flow; gEdges[gEdges[e].rev].w += min_flow; gFlow[gPre[u]][u] += min_flow; gFlow[u][gPre[u]] -= min_flow; } ans += min_flow; } for (e = gHead[u]; e != -1; e = gEdges[e].next){ if (gEdges[e].w > 0 && gDist[u] == gDist[gEdges[e].to] + 1) break; } if (e >= 0){ //可以向前找到一点,继续扩展 gPre[gEdges[e].to] = u; gPath[gEdges[e].to] = e; u = gEdges[e].to; } else{ if (--gGap[gDist[u]] == 0){ //gap 优化 break; } for (d = n, e = gHead[u]; e != -1; e = gEdges[e].next){ //重标号 if (gEdges[e].w > 0) d = min(d, gDist[gEdges[e].to]); } gDist[u] = d + 1; ++gGap[gDist[u]]; if (u != gSource) //回溯 u = gPre[u]; } } return ans; } int main(){ int u, v, w; int n, m; while (scanf("%d %d", &m, &n) != EOF){ gEdgeCount = 0; memset(gHead, -1, sizeof(gHead)); for (int i = 0; i < m; i++){ scanf("%d %d %d", &u, &v, &w); InsertEdge(u, v, w); } gSource = 1; gDestination = n; Bfs(); int result = ISPA(n); printf("%d ", result); } return 0; }