目录
1、理解增广路方法
Ford-Fulkerson方法基于增广路定理,当残量网络中中没有增广路时网络达到最大流,关于这些定理的理解和证明还得花些时间。我只能简单理解增广路就像将流量推回去一样,推回去的流量只要能流到汇点那就说明流量还可以增加。增广的方法是在网络中寻找一条从源点到汇点的增广路径,然后修改路径上的每条边的流量。
class Edge{
int from, to;
long cap, dis;
Edge next, rEdge;
public Edge(int from, int to, long cap, long dis, Edge next, Edge rEdge) {
this.from = from;
this.to = to;
this.cap = cap;
this.dis = dis;
this.next = next;
this.rEdge = rEdge;
}
}
2、最大流算法
2.1 、Ford-Fulkerson算法
最大流算法中最开始学习的是Ford-Fulkerson算法,前面说了该方法基于增广路定理,所以算法只做一件事就是增广直到没有流量非0的增广路。
boolean[] used = new boolean[MAXN];
Edge[] head = new Edge[MAXN];
int sindex, tindex;
long ans;
private long dfs(int v, int t, long cap) {
if (v == t) {
return cap;
}
used[v] = true;
for (Edge e = head[v]; e != null; e = e.next){
if (!used[e.to] && e.cap > 0){
long a = dfs(e.to, t, min(cap, e.cap));
if (a > 0) {
e.cap -= a;
e.rEdge.cap += a;
return a;
}
}
}
return 0;
}
private void maxFlow() {
ans = 0;
while (true) {
Arrays.fill(used, false);
long f = dfs(sindex, tindex, INF);
if (f == 0) {
return;
}
ans += f;
}
}
2.2、ek算法
Ford-Fulkerson方法没有限制如何寻找非0的增广路,于是出现了ek算法,ek算法通过bfs查找非0的增广路径,利用了bfs的优点能快速找最短的增广路径。大致流程是bfs找到最短增广路径,然后修改路径上的流量,重复上面的步骤知道bfs找不到增广路径。
Edge[] head = new Edge[MAXN];
int sindex, tindex;
long ans;
long[] flow = new long[MAXN<<2];
Edge[] pre = new Edge[MAXN<<2];
private void ek(){
ans = 0;
while (true){
Arrays.fill(flow, 0);
Arrays.fill(pre, null);
LinkedList<Integer> que = new LinkedList<Integer>();
que.offer(sindex);
flow[sindex] = INF;
while (!que.isEmpty()) {
int f = que.poll();
if (f == tindex) {
break;
}
for (Edge e = head[f]; e != null; e = e.next) {
if (flow[e.to] == 0 && e.cap > 0) {
flow[e.to] = min(e.cap, flow[f]);
pre[e.to] = e.rEdge;
que.offer(e.to);
}
}
}
if (flow[tindex] == 0) {
return;
}
ans += flow[tindex];
Edge preEdge = pre[tindex];
while (preEdge!= null) {
preEdge.cap += flow[tindex];
preEdge.rEdge.cap -= flow[tindex];
preEdge = pre[preEdge.to];
}
}
}
2.3、dinic算法
ek算法中每次只能找到一条增广路径,找到增广路径之后需要修改流量,被修改的边会破坏一部分顶点的联通性,导致最短路发生变化。因为网络的结构固定,最大流也是固定的,无论增广路径的选择先后不影响最大流。所以一次计算所有同一个长度的增广路径,就可以避免dfs的重复搜索,也可以减少bfs的次数。因为增广路径先后顺序无关,所以可以通过dfs查找增广路径,同时修改增广路径上的流量。方法的流程大致为:先bfs查找最短增广路也就是标记每个顶点到源点的距离,然后对最短路径进行增广,最短路径可能有多条。重复以上步骤直到bfs没有从源点到汇点的路径。
Edge[] head = new Edge[MAXN];
int sindex, tindex;
long ans;
int[] level = new int[MAXN<<2];
Edge[] cur = new Edge[MAXN];// 当前弧优化
private void dinic(){
ans = 0;
while (true){
bfs();
if (level[tindex] == -1) {
return;
}
System.arraycopy(head,0, cur, 0, MAXN);
long f = 0L;
while ((f = dfs(sindex, tindex, INF))>0){
ans += f;
}
}
}
private long dfs(int v, int t, long cap) {
if (v == t) {
return cap;
}
for (Edge e = cur[v]; e != null; e = e.next){
cur[v] = e;
if (level[e.to] == level[v]+1 && e.cap > 0){
long d = dfs(e.to, t, min(cap, e.cap));
if (d > 0) {
e.cap -= d;
e.rEdge.cap += d;
return d;
}
}
}
return 0;
}
private void bfs() {
Arrays.fill(level, -1);
LinkedList<Integer> que = new LinkedList<Integer>();
level[sindex] = 0;
que.offer(sindex);
while (!que.isEmpty()){
int f = que.poll();
for (Edge e = head[f]; e != null; e = e.next){
if (level[e.to] == -1 && e.cap > 0) {
level[e.to] = level[f] + 1;
que.offer(e.to);
}
}
}
}
2.4、isap算法
在dinic算法中每次增广同一个长度的增广路径,然后再次计算最短路径。可以发现最短路径不会变短,只会越来越长,因此当一个顶点增广之后,它就应该调整到一条更长的路径上。为了方便,考虑到汇点的最短距离,当一个顶点增广之后,将其距离加1,直到,源点距离汇点的距离大于N,或者中间出现断层(某一个距离的顶点数量为0)时程序结束。方法的流程大致为:初始标记,各个顶点到汇点的最小距离;多路增广,一个顶点增广完之后将其从当前距离集合中删除添加到距离更大的集合中;重复增广直到源点到汇点没有距离大于N,或者出现断层。
Edge[] head = new Edge[MAXN];
int sindex, tindex;
long ans;
int[] level = new int[MAXN];
Edge[] cur = new Edge[MAXN];
int[] gap = new int[MAXN];
boolean hasGap;
private void isap() {
ans = 0;
bfs();
System.arraycopy(head, 0, cur, 0, MAXN);
while (!hasGap && level[sindex] <= N+2){
ans += dfs(sindex, INF);
}
}
private long dfs(int v, long cap) {
if (v == tindex) {
return cap;
}
long flow = 0;
for (Edge e = cur[v]; e != null; e = e.next){
cur[v] = e;
if (level[e.to] +1 == level[v] && e.cap > 0){
long d = dfs(e.to, min(cap-flow, e.cap));
if (d > 0){
e.cap -= d;
e.rEdge.cap += d;
flow+=d;
if (flow == cap) {
return cap;
}
}
}
}
// 当前点增广路径处理完毕
if (--gap[level[v]] == 0) {
hasGap = true; // 断层
}
gap[++level[v]]++;
cur[v] = head[v];
return flow;
}
private void bfs() {
Arrays.fill(level, 0);
Arrays.fill(gap, 0);
LinkedList<Integer> que = new LinkedList<Integer>();
que.offer(tindex);
gap[1] = level[tindex] = 1;
while (!que.isEmpty()){
int f = que.poll();
for (Edge e = head[f]; e != null; e = e.next){
if (level[e.to] == 0 && e.reEdge.cap > 0) {
level[e.to] = level[f]+1;
gap[level[e.to]]++;
que.offer(e.to);
}
}
}
}
3、二分图最大匹配
3.1、最大流解决二分图匹配
二分图最大匹配可以看成是最小顶点覆盖,即不存在一条边两个顶点都没有被标记。二分图最小顶点覆盖和最大流等价,因为如果还有更大的流,那么一定会通过两个未标记过的顶点。通过添加源点和汇点,然后计算网络的最大流就可以求出最大匹配。
3.2、匈牙利算法
匈牙利算法一般用于分配问题,关键点在于解决分配冲突,假设A和B都和C可以匹配,且A先C匹配了,那么在为B进行匹配时,就会产生冲突。解决冲突的办法就是,抢了你的对象,让你自己去找一个新的。当然,实际情况不是如此野蛮,抢别人对象之前还是会问一下,能把对象让给我么?不行,哦哪算了。在增广路上可以很简单的实现匈牙利算法,把已经匹配过的部分看出一个子图,算法的目标就是不断添加新的未匹配的顶点和边到该子图中,使子图尽可能大。大致流程为:选择一个未匹配的顶点,尝试将该顶点与相连的顶点匹配,如果相连的顶点没有匹配,那么匹配完成。否则对该相连顶点原来匹配的顶点进行递归。
Edge[] head = new Edge[MAXN];
boolean[] vis = new boolean[MAXN];
int[] match = new int[MAXN];
int binaryMatch() {
Arrays.fill(match, 0);
int res = 0;
for (int i = 1; i <= N; i++) {
if (match[i] == 0){
Arrays.fill(vis, false);
if(dfs(i)){
res++;
}
}
}
return res;
}
private boolean dfs(int i) {
vis[i] = true;
for (Edge e = head[i]; e != null; e = e.next){
int w = match[e.to];
if (!vis[w] && (w == 0 || dfs(w))) {
match[i] = e.to;
match[e.to] = i;
return true;
}
}
return false;
}
4、最小费用最大费用流
4.1、问题描述
最小费用最大流算法的思想是优先在选择最短路上的流,对于同样的流量f来说,如果存在更短的路径d,那么肯定是选择最短的路径使得d*f最小。在构图上需要注意反边的费用需要取反,这样才能保证费用也可以在增广路上计算。
4.2、ek最小费用最大流
根据最小费用最大流的求解方法,只要找到最短流然后尽可能增广就可了,因此只需要将ek算法中bfs,改成计算最短路的spfa。
Edge[] head = new Edge[MAXN];
boolean[] inq = new boolean[MAXN];
long[] dis = new long[MAXN];
Edge[] pre = new Edge[MAXN];
private void minCostFlow() {
long f = N;
ans = 0;
while (spfa()){
long minf = f;
Edge e = pre[sink];
while (e != null){
minf = min(minf, e.cap);
e = pre[e.from];
}
f -= minf;
ans += minf * dis[sink];
e = pre[sink];
while (e!= null){
e.cap -= minf;
e.reEdge.cap += minf;
e = pre[e.from];
}
}
}
private boolean spfa() {
Arrays.fill(inq, false);
Arrays.fill(dis, INF);
Arrays.fill(pre, null);
LinkedList<Integer> que = new LinkedList<Integer>();
que.offer(source);
inq[source] = true;
dis[source] = 0;
while (!que.isEmpty()){
int v = que.poll();
inq[v] = false;
for (Edge e = head[v]; e != null; e = e.next){
if (e.cap > 0 && dis[e.from] + e.cost < dis[e.to]){
dis[e.to] = dis[e.from] + e.cost;
pre[e.to] = e;
if (!inq[e.to]){
que.offer(e.to);
inq[e.to] = true;
}
}
}
}
return dis[sink] != INF;
}
4.2、 多路增广最小费用最大流
同最大流算法一样,同样可以使用多路增广提高ek算法的效率。进行多路增广时需要注意,一定要有限增广最短路径,所以可以使用项dinic分层的思想,保证增广的顺序。
这里计算了最短路之后,能增广的路径就限制为满足d[u]+w[u][v]=d[v]的路径。一般选择计算各点到汇点的距离。这里应该也可以采用ISAP直接在增广过程中修改距离方法,通过计算每个顶点最小的松弛量,当增广完之后修改顶点的距离(没有实现过,不知道是否可行)。算法的大致流程为:计算汇点到各点的距离,沿着最短路增广,无法增广时,重新计算最短距离。
int source, sink;
boolean[] used = new boolean[MAXN];
boolean[] inq = new boolean[MAXN];
long[] dis = new long[MAXN];
Edge[] head = new Edge[MAXN];
private void minCostMaxFlow() {
int flow = 0;
ans = 0;
while (spfa()){
Arrays.fill(used, false);
ans += dis[source] * aug(source, INF);
}
}
// 在最短路上进行多路增广
private long aug(int v, long cap) {
if (v == sink){
return cap;
}
used[v] = true;
int f = 0;
for (Edge e = head[v]; e != null; e = e.next){
if (!used[e.to] && e.cap > 0 && dis[v] == dis[e.to] + e.dis){
long d = aug(e.to, min(cap-f, e.cap));
e.cap -= d;
e.rEdge.cap += d;
f += d;
if (f == cap){
break;
}
}
}
return f;
}
// 汇点到其他点的最小距离
private boolean spfa() {
Arrays.fill(inq, false);
Arrays.fill(dis, INF);
LinkedList<Integer> que = new LinkedList<Integer>();
que.offer(sink);
inq[sink] = true;
dis[sink] = 0;
while (!que.isEmpty()){
int cur = que.poll();
inq[cur] = false;
for (Edge e = head[cur]; e != null; e = e.next){
if (e.rEdge.cap > 0 && dis[e.to] > dis[cur] - e.dis){
dis[e.to] = dis[cur] - e.dis;
if (!inq[e.to]){
inq[e.to] = true;
if (que.isEmpty() || dis[que.peek()] <= dis[e.to]){
que.offer(e.to);
} else {
que.addFirst(e.to);
}
}
}
}
}
return dis[source] != INF;
}
4.3、zkw费用流
zkw费用流计算最短路径没有使用spfa最短路算法,使用类似KM算法用标号的方式计算最短路径。为了方便记忆我把重标号的过程理解为,估计S到T的最短路径,然后不断增大这个估计值。例如S出去的几条边中最短的一条边长为Ci,那么S到T的最短路径至少是Ci,所以先假设最短路径是Ci,在该最短路径上增广,如果无法到达T,则表明最短路径比Ci大。update的目的就是找到一个最短路径的估计值,为了保证不错过任何一个最短路径,估计值的增量要尽可能小,所以要选择一个最短边的权值作为估计值的增量。整个过程就像有点像prime最小生成树的过程,从S出发每次选择一条最短边连接到S或者间接链接到S,这样保证一定会枚举到所有最短路径。
long ans;
int N;
int source, sink;
Edge[] head = new Edge[MAXN];
boolean[] vis = new boolean[MAXN];
int INF = 1000000007;
int dis = 0;
private int zkw() {
dis = 0;
int flow = 0;
while (true){
while (true){
Arrays.fill(vis, false);
long f = aug(source, INF);
if (f <= 0) {
break;
}
ans += dis * f;
flow += f;
}
if (!update()){
break;
}
}
return flow;
}
private boolean update(){
long d = INF;
for (int i = 0; i < N; i++) {
if (vis[i]) {
for (Edge e = head[i]; e != null; e = e.next) {
if (e.cap > 0 && !vis[e.to]) {
d = min(d, e.dis);
}
}
}
}
if (d == INF) {
return false;
}
for (int i = 0; i < N; i++) {
if (vis[i]) {
for (Edge e = head[i]; e != null; e = e.next) {
e.dis -= d;
e.reEdge.dis += d;
}
}
}
dis += d;
return true;
}
// 在最短路上进行多路增广
private long aug(int v, long cap) {
if (v == sink){
return cap;
}
vis[v] = true; // dfs标记
int flow = 0;
for (Edge e = head[v]; e != null; e = e.next){
if (!vis[e.to] && e.cap > 0 && e.dis == 0){
long f = aug(e.to, min(cap-flow, e.cap));
e.cap -= f;
e.reEdge.cap += f;
if ((flow += f) == cap){
break;
}
}
}
return flow;
}
zkw中重标号可以先计算出最短路径,然后更新标号,这样能保证每次重标号之后一定有从源点到汇点的最短路径。注意这里和普通SPFA增广算法的区别,这里计算最短路是为了重标号。
int[] disSPFA = new int[MAXN];
private boolean updateSPFA(){
Arrays.fill(disSPFA, INF);
LinkedList<Integer> que = new LinkedList<Integer>();
que.addLast(sink);
disSPFA[sink] = 0;
while (!que.isEmpty()) {
int now = que.poll(), d = 0;
for (Edge e = head[now]; e != null; e = e.next) {
if (e.reEdge.cap > 0 && (d = disSPFA[now] -e.dis) < disSPFA[e.to]){
disSPFA[e.to] = d;
if (que.isEmpty() || d < disSPFA[que.getFirst()]) {
que.addFirst(e.to);
} else {
que.addLast(e.to);
}
}
}
}
for (int i = 0; i < N << 1; i++) {
for (Edge e = head[i]; e != null; e = e.next) {
e.dis += disSPFA[e.to] - disSPFA[i];
}
}
dis += disSPFA[source];
return disSPFA[source] < INF;
}
5、二分图带权最大匹配
5.1、最小费用最大流
最小费用最大流可以计算带权匹配,和二分图最大匹配一样,只是要考虑每条边的距离。
5.2、KM算法
KM算法和zkw一样,不直接求最短路或者最长路,而是通过不断扩大相等子图,然后计算相等子图的完全匹配,从而得到最大带权匹配。相等子图上的顶点标号可以更改,只要同时修改边的两个端点就不会破坏相等子图的定义。因此可以通过修改相等子图中顶点标号,从而将不在相等子图中的顶点和边添加到相等子图中。
long[][] map = new long[MAXN][MAXN];
int[] match = new int[MAXN];
boolean[] visx = new boolean[MAXN];
boolean[] visy = new boolean[MAXN];
long[] lablex = new long[MAXN];
long[] labley = new long[MAXN];
long minz = INF;
long[] slack = new long[MAXN];
void km(){
Arrays.fill(match, -1);
Arrays.fill(labley, 0);
for (int i = 0; i < N; i++) {
lablex[i] = -INF;
for (int j = 0; j < N * M; j++) {
lablex[i] = max(lablex[i], map[i][j]);
}
}
for (int i = 0; i < N; i++) {
Arrays.fill(slack, INF);
while (true){
minz = INF;
Arrays.fill(visx, false);
Arrays.fill(visy, false);
if(dfs(i)) {
break;
}
for (int j = 0; j < N * M; j++) {
if (!visy[j]){
minz = min(minz, slack[j]);
}
}
for (int j = 0; j < N; j++) {
if (visx[j]) {
lablex[j] -= minz;
}
}
for (int j = 0; j < N * M; j++) {
if (visy[j]) {
labley[j] += minz;
} else {
slack[j] -= minz;
}
}
}
}
ans = 0;
for (int i = 0; i < N * M; i++) {
if (match[i] >= 0){
ans += map[match[i]][i];
}
}
}
private boolean dfs(int x) {
visx[x] = true;
for (int y = 0; y < N * M; y++) {
if (!visy[y] && map[x][y] != INF) {
long t = lablex[x] + labley[y] - map[x][y];
if (t == 0){
visy[y] = true;
if (match[y] == -1 || !visx[match[y]] && dfs(match[y])){
match[y] = x;
return true;
}
} else if (t > 0){
slack[y] = min(slack[y], t);
}
}
}
return false;
}
5.3、BFS版本的KM算法
新增加的边不一定非要到增加到最后一个顶点,在搜索路径上任意一个顶点,只要找到一条新增的边都可以直接增加。使用BFS没有了DFS中匹配的状态,所以需要保存状态回溯更新各个顶点的匹配。例如,原本的匹配时A-B,当A找到一个新的匹配A-C后,那么B就空出来了,需要记录C匹配之后B就会空出来,同样B也需要记录之前的那些状态。
int[] match = new int[MAXN];
boolean[] visy = new boolean[MAXN];
long[] lablex = new long[MAXN];
long[] labley = new long[MAXN];
long[] slack = new long[MAXN];
int[] pre = new int[MAXN];
void km(){
Arrays.fill(match, -1);
Arrays.fill(labley, 0);
for (int i = 0; i < N; i++) {
lablex[i] = -INF;
for (int j = 0; j < N; j++) {
lablex[i] = max(lablex[i], map[i][j]);
}
}
for (int i = 0; i < N; i++) {
bfs(i);
}
ans = 0;
for (int i = 0; i < N; i++) {
if (match[i] >= 0){
ans += map[match[i]][i];
}
}
}
private void bfs(int x){
Arrays.fill(slack, INF);
Arrays.fill(visy, false);
long d;
int y = N; // 临时使用
match[y] = x;
int ny = N;
while (x!=-1){
// 找x的匹配
d = INF;
visy[y] = true;
for (int i = 0; i < N; i++) {
if (!visy[i] && map[x][i] != INF){
if (lablex[x] + labley[i] - map[x][i] < slack[i]) {
slack[i] = lablex[x] + labley[i] - map[x][i];
pre[i] = y; // 记录i应该和match[y]匹配
}
if (slack[i] < d) {
d = slack[i];
ny = i;
}
}
}
for (int i = 0; i <= N; i++) {
if (visy[i]){
lablex[match[i]] -= d;
labley[i] += d;
} else {
slack[i] -= d;
}
}
y = ny;
x = match[y];
}
while (y<N){
match[y] = match[pre[y]];
y = pre[y];
}
}
6、其他细节
6.1、dijskra最小费用流
如果最小费用流中有负边,那么就不能用dijskra计算最短路,这里对负边的解释为初始状态下的负边,例如,要求最大值时,会将代价取反,这时就认为是一个负边。解决负边的一个简单办法就是将负边满流,满流的意思就是将反向边的容量设为最大。满流之后为了保证连通性,还需要从源点和汇点向该边添加一条代价为0的路径。例如,添加一条<u,v>的负边,那么需要满流之后,需要添加源点到v的边以及汇点到u的边。满流解决负边的方法,不仅可以用来使用dijiskra计算最短距离,在zkw费用流中好像也可以使用。
6.2、消负圈
在最小费用最大流中,是不存在负圈的。可以证明如果存在负圈,那么通过负圈得到的费用更小,而流量不变。消负圈可以用来检测最小费用最大流。消负圈的方法有Bellman-Ford算法、spfa算法、Floyd - Warshall。
6.3、当前弧优化
多路增广中通常一个顶点增广完之后,下次就不需要增广了,所以可以保存当前顶点哪些弧已经增广完毕了,下次直接从能增广的弧开始增广。使用当前弧优化,要注意已经增广过的弧一定是已经增广完毕的,像zkw中通过used访问标志会导致某一个弧,因为访问的次序靠后所以没有增广就返回了。
7、练习题
洛谷 P3376 P4014 (可以下载数据,用来测试模板)
poj 3041、3075、3281、 3469、 2135、 2175、 3686、 3680