这些天学习网络流,总结了一下用到的主要算法,主要从下面几个方面来介绍
一、常见的几种算法
二、这些算法的复杂度
三、这些算法适合处理的问题
四、算法模板
所有增广路径问题都是以Ford_Fulkerson方法为基础,之所以称为方法而不是算法,因为它提供的是一种思想。
Ford_Fulkerson(s,t) f = 0,对自定义流f进行初始化 while 存在增广路径p do 沿p对流f进行增广 //f += MaxFlow,MaxFlow为增广路径中最大流 return p; //p即为整个图中最大流我原来这个方法一直没有得到很好的理解,汗,现在才明白为什么叫增广路径,f是自己定义的一个流,原来图中存在容量c(i,j),在不超过c(i,j)的情况下尽情对f流进行增光,这样得到的f即为最大流。残留容量cf(i, j) = c(i, j) - f(i, j),网中所有cf(i, j) > 0 的边组成一个残留网络,每次对f进行一次增广,则cf(i, j) -= flow; cf(j, i) += flow;对反向边进行操作也是后面才了解,是为了给后面查找增广路径时候提供更多的选择,如果这个点不通了,则返回到上一个节点继续查找。这样最后残留网络中反向边流量cf(j, i) = f(i, j)。增广路径即s 到 t的一条路径。
这是一类算法,每次寻找最短最广路径进行增广,后面的DK,Dinic算法都是基于此
Sap() ans = 0 while 寻找增广路径 do 存在一条最短增广路径p flow = min(cf(i, j) [i,j] ∈ E) ans += flow 沿p进行增广 //正反两步 return ans;
BFS寻找最短最广路径,最简单的一种寻找最短最广路径方法,算法复杂度为O(V*E^2),因为BFS寻找最短路径需要搜索完所有小于最短距离的边才能找到终点,所以算法复杂度比较高。
算法模板:
int path[nMax]; int queue[nMax]; int flow[nMax]; int s, t; ek_Bfs()//算法复杂度O(E) { int front, rear; front = rear = 0; queue[rear ++] = s; flow[s] = INF; while(front < rear) { int u = queue[front ++]; if(u == t) break; for(int v = 0; v < n; ++ v)//其实就是搜索临边的意思 { if(path[v] == -1 && map[u][v]) { flow[v] = min(flow[u], map[u][v]); path[v] = u; queue[rear ++] = v; } } } if(path[t] == -1) return 0; else return flow[t]; } ek_Flow()//算法复杂度O(V*E) { int MaxFlow = 0; while((flow = ek_Bfs()) { MaxFlow += flow; int cur = t; while(cur != s) { int pre = path[cur]; map[pre][cur] -= flow; map[cur][pre] += flow; cur = pre; } } return MaxFlow; }
算法思路:BFS寻找终点太慢,DFS又无法保证搜索到最短路径,结合这两种算法的优势,利用构造分层网络的算法来寻找最短路径,这个算法又称为阻塞流算法。
首先从s点处进行bfs搜索,dis[s] = 0,然后层次数依次增加,如果搜索到t,则结束。dfs进行搜索,逐层搜索,进行d[v] == d[u] + 1判断,任意一条路径即为最短路径。
建立层次图最大的作用就是规范DFS搜索,使尽可能向终点进行搜索。
Dinic 算法的另一个优化之处:找一条增广路径同时可以找到多条,类似增广路径树。如果cur顶点的总残余流量不为零,这样我们就不必要从起点再次开始寻找增广路,而是从cur顶点出发直接开始,这样就会减少了重复计算,提高效率。
时间复杂度:O(V^2*E)
模板:
struct Adj { int v, w; int next; }adj[mMax];//总边数 int head[nMax]; int cnt; int dis[nMax]; int bfs(int s, int d) { queue<int> que; que.push(s); memset(dis, -1, sizeof(dis)); dis[s] = 0; int i; while(!que.empty()) { int u = que.front(); que.pop(); for(i = head[u]; i != -1; i = adj[i].next) { int v = adj[i].v; if(adj[i].w && dis[v] == -1) { dis[v] = dis[u] + 1; que.push(v); } } } return dis[d]; } int min(int a, int b) { return a < b ? a : b; } int dfs(int s, int d, int cost) { if(s == d) return cost; int t, ans = 0; int i; for(i = head[s]; i != -1; i = adj[i].next) { int v = adj[i].v; if(dis[v] == dis[s] + 1 && adj[i].w && (t = dfs(v, d, min(adj[i].w, cost))) != -1) //①原来这里漏写了adj[i].w != 0,结果超时 { adj[i].w -= t; adj[i ^ 1].w += t;//③ cost -= t; ans += t; if(!cost) break; } } return ans; } int dinic(int s, int d)//s:源点,d:汇点 { int ans = 0; while(bfs(s, d) != -1) { ans += dfs(s, d, INF); } return ans; }
算法思路:Dinic算法效率已经很高,然而算法的优化时无尽头的,Dinic算法需要多次计算层次图,增加了复杂度,是不是可以不多次计算层次图呢?答案是肯定的,这就是ISAP算法。
ISAP计算的是反向图的层次图,作用与原图的层次图一样。计算反向图的层次图是便于重新给顶点标号,即计算层次图,具体做法:在查找<u,v>的时候,MinDis = min(dis[v]),这样查找玩所有与顶点u相关的边之后,dis[u] = dis[v] + 1,从而得到新的层次图。在刚开始寻找的时候,我们也不需要计算层次图,对所有dis[]都赋值为0即可,对效率没有多大影响。
另外ISAP算法的另一个优化在于:如果层次图出现断层,则直接结束。
时间复杂度:O(V^2*E)
模板:
struct Adj { int v, w; int next; }adj[mMax]; int head[nMax]; int cnt; int num[nMax];//每一层顶点的个数,便于判断层次图中是否出现断层 int dis[nMax]; int NN;//总顶点数 int min(int a, int b) { return a < b ? a : b; } int dfs(int u, int s, int d, int cost) { if(u == d) return cost; int i; int ans = 0; int _min = NN; for(i = head[u]; i != -1; i = adj[i].next) { int v = adj[i].v; if(adj[i].w) { if(dis[v] + 1 == dis[u]) { int t = dfs(v, s, d, min(adj[i].w, cost)); adj[i].w -= t; adj[i ^ 1].w += t; cost -= t; ans += t; if(dis[s] == NN) return ans; if(!cost) break; } if(_min > dis[v]) _min = dis[v]; } } if(!ans) { if(-- num[dis[u]] == 0) dis[s] = NN; dis[u] = _min + 1; ++ num[dis[u]]; } return ans; } int isap(int s, int d) { memset(dis, 0, sizeof(dis)); memset(num, 0, sizeof(num)); num[0] = NN; int ans = 0; while(dis[s] < NN)//dis[s] == NV其实是你自己定义的一个终止条件,你也可以使用flag标记表示出现断层 ans += dfs(s, s, d, INF); return ans; }
int link[nMax]; int useif[nMax]; int n, m;//A、B集合分别对应的顶点数 int getPath(int x)//寻找匹配边,算法复杂度O(m^2),即O(V^2) { for(int i = 0; i < m; ++ i) { if(!useif[i] && map[x][i]) { useif[i] = 1; if(link[i] == -1 && getPath(link[i]))//如果点i被匹配,检查点link[i]是否存在另外可匹配边 { link[i] = x; return 1; } } } return 0; } int getNum()//返回最大匹配数,算法复杂度O(n),即O(V) { int num = 0; memset(link, -1, sizeof(link)); for(int i = 0; i < n; ++ i) { memset(useif, 0, sizeof(useif)); num += getPath(i); } return num; }
以上内容只是我的理解,这两篇博文总结的很不错,推荐一下!
http://www.cnblogs.com/longdouhzt/archive/2012/05/20/2510753.html
http://www.cnblogs.com/longdouhzt/archive/2012/05/20/2510743.html
图论继续学习:
http://starfall512.com/?p=272#comment-152