http://poj.org/problem?id=3469
SAP算法
关于网络流的定义之类可以到nocow寻找,这里不在阐述。
SAP算法(by dd_engi):求最大流有一种经典的算法,就是每次找增广路时用BFS找,保证找到的增广路是弧数最少的,也就是所谓的Edmonds-Karp算 法。可以证明的是在使用最短路增广时增广过程不超过V*E次,每次BFS的时间都是O(E),所以Edmonds-Karp的时间复杂度就是 O(V*E^2)。
如果能让每次寻找增广路时的时间复杂度降下来,那么就能提高算法效率了,使用距离标号的最短增广路算法就是这样的。所谓距离标号,就是某个点到汇点的最少 的弧的数量(另外一种距离标号是从源点到该点的最少的弧的数量,本质上没什么区别)。设点i的标号为D[i],那么如果将满足D[i]=D[j]+1的弧 (i,j)叫做允许弧,且增广时只走允许弧,那么就可以达到“怎么走都是最短路”的效果。每个点的初始标号可以在一开始用一次从汇点沿所有反向边的BFS 求出,实践中可以初始设全部点的距离标号为0,问题就是如何在增广过程中维护这个距离标号。
维护距离标号的方法是这样的:当找增广路过程中发现某点出发没有允许弧时,将这个点的距离标号设为由它出发的所有弧的终点的距离标号的最小值加一。这种维 护距离标号的方法的正确性我就不证了。由于距离标号的存在,由于“怎么走都是最短路”,所以就可以采用DFS找增广路,用一个栈保存当前路径的弧即可。当 某个点的距离标号被 改变时,栈中指向它的那条弧肯定已经不是允许弧了,所以就让它出栈,并继续用栈顶的弧的端点增广。为了使每次找增广路的时间变成均摊 O(V),还有一个重要的优化是对于每个点保存“当前弧”:初始时当前弧是邻接表的第一条弧;在邻接表中查找时从当前弧开始查找,找到了一条允许弧,就把 这条弧设为当前弧;改变距离标号时,把当前弧重新设为邻接表的第一条弧,还有一种在常数上有所优化的写法是改变距离标号时把当前弧设为那条提供了最小标号 的弧。当前弧的写法之所以正确就在于任何时候我们都能保证在邻接表中当前弧的前面肯定不存在允许弧。
还有一个常数优化是在每次找到路径并增广完毕之后不要将路径中所有的顶点退栈,而是只将瓶颈边以及之后的边退栈,这是借鉴了Dinic算法的思想。注意任 何时候待增广的“当前点”都应该是栈顶的点的终点。这的确只是一个常数优化,由于当前边结构的存在,我们肯定可以在O(n)的时间内复原路径中瓶颈边之前 的所有边。
优化:
1.邻接表优化:
如果顶点多的话,往往N^2存不下,这时候就要存边:
存每条边的出发点,终止点和价值,然后排序一下,再记录每个出发点的位置。以后要调用从出发点出发的边时候,只需要从记录的位置开始找即可(其实可以用链表)。优点是时间加快空间节省,缺点是编程复杂度将变大,所以在题目允许的情况下,建议使用邻接矩阵。
2.GAP优化:
如果一次重标号时,出现距离断层,则可以证明ST无可行流,此时则可以直接退出算法。
3.当前弧优化:
为了使每次找增广路的时间变成均摊O(V),还有一个重要的优化是对于每个点保存“当前弧”:初始时当前弧是邻接表的第一条弧;在邻接表中查找时从当前弧开始查找,找到了一条允许弧,就把这条弧设为当前弧;改变距离标号时,把当前弧重新设为邻接表的第一条弧。
/* 有一些模块(modules)和一个双核处理器,一个模块可以在任意一个核上处理,每个核对应每个模块有个开销。现在有一些模块间需要数据交换, 如果需要数据交换的模块在一个核上处理,则不需要额外开销,否则需要加上一个开销。现在需要完成所有模块,问最小需要多少开销。 如果没有这个额外的开销,那么每个模块只要选择开销小的那个核就行了。额外的开销给选择加上了限制。 */ #include <iostream> #include <cstdio> //#include <memory.h> using namespace std; #define MAXN 30020 #define MAXE 2000000 #define INF 0x3fffffff int ne, nv, index, s, t, net[MAXN]; struct Edge{ int next,pair; int v,cap,flow; }edge[MAXE]; void add(const int& u,const int& v,const int& val) { edge[index].next = net[u];//增加正向边 net[u] = index; edge[index].v = v; edge[index].cap = val; edge[index].flow = 0; edge[index].pair = index+1; ++index; edge[index].next = net[v];//增加反向边 net[v] = index; edge[index].v = u; edge[index].cap = 0; edge[index].flow = 0; edge[index].pair = index - 1; ++index; } int ISAP() { long numb[MAXN], dist[MAXN], curedge[MAXN], pre[MAXN];//dist[i]表示节点i到汇点的最少边数,pre[i]表示i的前驱节点,numb[i]表示最少边数为i的节点个数 long cur_flow, max_flow, u, tmp, neck, i; memset(dist,0,sizeof(dist)); memset(numb,0,sizeof(numb)); for(i = 1 ; i <= nv ; ++i) curedge[i] = net[i]; numb[nv] = nv; max_flow = 0; u = s; while(dist[s] < nv) { /* first , check if has augmemt flow */ if(u == t)//找到一条增广路径 { cur_flow = INF; for(i = s; i != t;i = edge[curedge[i]].v) { if(cur_flow > edge[curedge[i]].cap) { neck = i; cur_flow = edge[curedge[i]].cap; } } for(i = s; i != t; i = edge[curedge[i]].v) { tmp = curedge[i]; edge[tmp].cap -= cur_flow; edge[tmp].flow += cur_flow; tmp = edge[tmp].pair; edge[tmp].cap += cur_flow; edge[tmp].flow -= cur_flow; } max_flow += cur_flow; u = s; } /* if .... else ... */ for(i = curedge[u]; i != -1; i = edge[i].next)//寻找“允许弧” if(edge[i].cap > 0 && dist[u] == dist[edge[i].v]+1) break; if(i != -1) { curedge[u] = i; pre[edge[i].v] = u; u = edge[i].v; } else { if(0 == --numb[dist[u]]) break; curedge[u] = net[u]; for(tmp = nv,i = net[u]; i != -1; i = edge[i].next) if(edge[i].cap > 0) tmp = tmp<dist[edge[i].v]?tmp:dist[edge[i].v]; dist[u] = tmp + 1; ++numb[dist[u]]; if(u != s) u = pre[u]; } } return max_flow; } int main() { freopen("in.txt", "r", stdin); int n, m, i, j, val, a, b; index = 0; scanf("%d%d", &n, &m); s = n + 1; t = n + 2; nv = t; memset(net, -1, sizeof(net));// for(i = 1; i <= n; ++i) { scanf("%d%d", &a, &b); add(s,i,a); add(i,t,b); } for(i = 1;i <= m; ++i) { //正向边 scanf("%d%d%d",&a,&b,&val); edge[index].next = net[a]; net[a] = index; edge[index].v = b; edge[index].cap = val; edge[index].flow = 0; edge[index].pair = index+1; //反向边 ++index; edge[index].next = net[b]; net[b] = index; edge[index].v = a; edge[index].cap = val; edge[index].flow = 0; edge[index].pair = index - 1; ++index; } printf("%d/n",ISAP()); return 0; }