首先,我们引入一个引理——最短路非递减引理。具体地,我们记 df(u) 为 Gf 上结点 u 到源点 s 的距离(即最短路长度,下同)。对于某一轮增广,我们用 f 和 f′ 分别表示增广前的流和增广后的流,我们断言,对于任意结点 u,增广总是使得 df′(u)≥df(u)。我们将在稍后证明这一引理。
注意到在 GL 上 DFS 的过程中,如果结点 u 同时具有大量入边和出边,并且 u 每次接受来自入边的流量时都遍历出边表来决定将流量传递给哪条出边,则 u 这个局部的时间复杂度最坏可达 O(∣E∣2)。为避免这一缺陷,如果某一时刻我们已经知道边 (u,v) 已经增广到极限(边 (u,v) 已无剩余容量或 v 的后侧已增广至阻塞),则 u 的流量没有必要再尝试流向出边 (u,v)。据此,对于每个结点 u,我们维护 u 的出边表中第一条还有必要尝试的出边。习惯上,我们称维护的这个指针为当前弧,称这个做法为当前弧优化。
多路增广
多路增广是 Dinic 算法的一个常数优化——如果我们在层次图上找到了一条从 s 到 t 的增广路 p,则接下来我们未必需要重新从 s 出发找下一条增广路,而可能从 p 上最后一个仍有剩余容量的位置出发寻找一条岔路进行增广。考虑到其与回溯形式的一致性,这一优化在 DFS 的代码实现中也是自然的。
我们称节点 r 是参考节点当且仅当 p(r)=minp(v)。对于一个参考节点 r,我们一定可以让经过 r 的流量增加 p(r) 以使其容量变为 0。这是因为 L 是有向无环图且 L 中节点容量至少为 p(r),所以我们一定能找到一条从 s 经过 r 到达 t 的有向路径。那么我们让这条路上的边流量都增加 p(r) 即可。这条路即为这一阶段的增广路。寻找增广路可以用 BFS。增广完之后所有满流边都可以从 L 中删除,因为它们不会在此阶段后被使用。同样,所有与 s 和 t 不同且没有出边或入边的节点都可以删除。
时间复杂度分析
MPM 算法的每个阶段都需要 O(V2),因为最多有 V 次迭代(因为至少删除了所选的参考节点),并且在每次迭代中,我们删除除最多 V 之外经过的所有边。求和,我们得到 O(V2+E)=O(V2)。由于阶段总数少于 V,因此 MPM 算法的总运行时间为 O(V3)。
阶段总数小于 V 的证明
MPM 算法在少于 V 个阶段内结束。为了证明这一点,我们必须首先证明两个引理。
引理 1:每次迭代后,从 s 到每个点的距离不会减少,也就是说,leveli+1[v]≥leveli[v]。
证明:固定一个阶段 i 和点 v。考虑 GiR 中从 s 到 v 的任意最短路径 P。P 的长度等于 leveli[v]。注意 GiR 只能包含 GiR 的后向边和前向边。如果 P 没有 GiR 的后边,那么 leveli+1[v]≥leveli[v]。因为 P 也是 GiR 中的一条路径。现在,假设 P 至少有一个后向边且第一个这样的边是 (u,w),那么 leveli+1[u]≥leveli[u](因为第一种情况)。边 (u,w) 不属于 GiR,因此 (u,w) 受到前一次迭代的增广路的影响。这意味着 leveli[u]=leveli[w]+1。此外,leveli+1[w]=leveli+1[u]+1。从这两个方程和 leveli+1[u]≥leveli[u] 我们得到 leveli+1[w]≥leveli[w]+2。路径的剩余部分也可以使用相同思想。
struct MPM { struct FlowEdge { int v, u; long long cap, flow; FlowEdge() {} FlowEdge(int _v, int _u, long long _cap, long long _flow) : v(_v), u(_u), cap(_cap), flow(_flow) {} FlowEdge(int _v, int _u, long long _cap) : v(_v), u(_u), cap(_cap), flow(0ll) {} }; constexpr static long long flow_inf = 1e18; vector<FlowEdge> edges; vector<char> alive; vector<long long> pin, pout; vector<list<int>> in, out; vector<vector<int>> adj; vector<long long> ex; int n, m = 0; int s, t; vector<int> level; vector<int> q; int qh, qt; void resize(int _n) { n = _n; ex.resize(n); q.resize(n); pin.resize(n); pout.resize(n); adj.resize(n); level.resize(n); in.resize(n); out.resize(n); } MPM() {} MPM(int _n, int _s, int _t) { resize(_n); s = _s; t = _t; } void add_edge(int v, int u, long long cap) { edges.push_back(FlowEdge(v, u, cap)); edges.push_back(FlowEdge(u, v, 0)); adj[v].push_back(m); adj[u].push_back(m + 1); m += 2; } bool bfs() { while (qh < qt) { int v = q[qh++]; for (int id : adj[v]) { if (edges[id].cap - edges[id].flow < 1) continue; if (level[edges[id].u] != -1) continue; level[edges[id].u] = level[v] + 1; q[qt++] = edges[id].u; } } return level[t] != -1; } long long pot(int v) { return min(pin[v], pout[v]); } void remove_node(int v) { for (int i : in[v]) { int u = edges[i].v; auto it = find(out[u].begin(), out[u].end(), i); out[u].erase(it); pout[u] -= edges[i].cap - edges[i].flow; } for (int i : out[v]) { int u = edges[i].u; auto it = find(in[u].begin(), in[u].end(), i); in[u].erase(it); pin[u] -= edges[i].cap - edges[i].flow; } } void push(int from, int to, long long f, bool forw) { qh = qt = 0; ex.assign(n, 0); ex[from] = f; q[qt++] = from; while (qh < qt) { int v = q[qh++]; if (v == to) break; long long must = ex[v]; auto it = forw ? out[v].begin() : in[v].begin(); while (true) { int u = forw ? edges[*it].u : edges[*it].v; long long pushed = min(must, edges[*it].cap - edges[*it].flow); if (pushed == 0) break; if (forw) { pout[v] -= pushed; pin[u] -= pushed; } else { pin[v] -= pushed; pout[u] -= pushed; } if (ex[u] == 0) q[qt++] = u; ex[u] += pushed; edges[*it].flow += pushed; edges[(*it) ^ 1].flow -= pushed; must -= pushed; if (edges[*it].cap - edges[*it].flow == 0) { auto jt = it; ++jt; if (forw) { in[u].erase(find(in[u].begin(), in[u].end(), *it)); out[v].erase(it); } else { out[u].erase(find(out[u].begin(), out[u].end(), *it)); in[v].erase(it); } it = jt; } else break; if (!must) break; } } } long long flow() { long long ans = 0; while (true) { pin.assign(n, 0); pout.assign(n, 0); level.assign(n, -1); alive.assign(n, true); level[s] = 0; qh = 0; qt = 1; q[0] = s; if (!bfs()) break; for (int i = 0; i < n; i++) { out[i].clear(); in[i].clear(); } for (int i = 0; i < m; i++) { if (edges[i].cap - edges[i].flow == 0) continue; int v = edges[i].v, u = edges[i].u; if (level[v] + 1 == level[u] && (level[u] < level[t] || u == t)) { in[u].push_back(i); out[v].push_back(i); pin[u] += edges[i].cap - edges[i].flow; pout[v] += edges[i].cap - edges[i].flow; } } pin[s] = pout[t] = flow_inf; while (true) { int v = -1; for (int i = 0; i < n; i++) { if (!alive[i]) continue; if (v == -1 || pot(i) < pot(v)) v = i; } if (v == -1) break; if (pot(v) == 0) { alive[v] = false; remove_node(v); continue; } long long f = pot(v); ans += f; push(v, s, f, false); push(v, t, f, true); alive[v] = false; remove_node(v); } } return ans; }};
ISAP
在 Dinic 算法中,我们每次求完增广路后都要跑 BFS 来分层,有没有更高效的方法呢?
答案就是下面要介绍的 ISAP 算法。
过程
和 Dinic 算法一样,我们还是先跑 BFS 对图上的点进行分层,不过与 Dinic 略有不同的是,我们选择在反图上,从 t 点向 s 点进行 BFS。
具体来说,设 i 号点的层为 di,当我们结束在 i 号点的增广过程后,我们遍历残量网络上 i 的所有出边,找到层最小的出点 j,随后令 di←dj+1。特别地,若残量网络上 i 无出边,则 di←n。
容易发现,当 ds≥n 时,图上不存在增广路,此时即可终止算法。
和 Dinic 类似,ISAP 中也存在 当前弧优化。
而 ISAP 还存在另外一个优化,我们记录层数为 i 的点的数量 numi,每当将一个点的层数从 x 更新到 y 时,同时更新 num 数组的值,若在更新后 numx=0,则意味着图上出现了断层,无法再找到增广路,此时可以直接终止算法(实现时直接将 ds 标为 n),该优化被称为 GAP 优化。
实现
参考代码
struct Edge { int from, to, cap, flow; Edge(int u, int v, int c, int f) : from(u), to(v), cap(c), flow(f) {}};bool operator<(const Edge& a, const Edge& b) { return a.from < b.from || (a.from == b.from && a.to < b.to);}struct ISAP { int n, m, s, t; vector<Edge> edges; vector<int> G[MAXN]; bool vis[MAXN]; int d[MAXN]; int cur[MAXN]; int p[MAXN]; int num[MAXN]; void AddEdge(int from, int to, int cap) { edges.push_back(Edge(from, to, cap, 0)); edges.push_back(Edge(to, from, 0, 0)); m = edges.size(); G[from].push_back(m - 2); G[to].push_back(m - 1); } bool BFS() { memset(vis, 0, sizeof(vis)); queue<int> Q; Q.push(t); vis[t] = true; d[t] = 0; while (!Q.empty()) { int x = Q.front(); Q.pop(); for (int i = 0; i < G[x].size(); i++) { Edge& e = edges[G[x][i] ^ 1]; if (!vis[e.from] && e.cap > e.flow) { vis[e.from] = true; d[e.from] = d[x] + 1; Q.push(e.from); } } } return vis[s]; } void init(int n) { this->n = n; for (int i = 0; i < n; i++) G[i].clear(); edges.clear(); } int Augment() { int x = t, a = INF; while (x != s) { Edge& e = edges[p[x]]; a = min(a, e.cap - e.flow); x = edges[p[x]].from; } x = t; while (x != s) { edges[p[x]].flow += a; edges[p[x] ^ 1].flow -= a; x = edges[p[x]].from; } return a; } int Maxflow(int s, int t) { this->s = s; this->t = t; int flow = 0; BFS(); memset(num, 0, sizeof(num)); for (int i = 0; i < n; i++) num[d[i]]++; int x = s; memset(cur, 0, sizeof(cur)); while (d[s] < n) { if (x == t) { flow += Augment(); x = s; } int ok = 0; for (int i = cur[x]; i < G[x].size(); i++) { Edge& e = edges[G[x][i]]; if (e.cap > e.flow && d[x] == d[e.to] + 1) { ok = 1; p[e.to] = G[x][i]; cur[x] = i; x = e.to; break; } } if (!ok) { int m = n - 1; for (int i = 0; i < G[x].size(); i++) { Edge& e = edges[G[x][i]]; if (e.cap > e.flow) m = min(m, d[e.to]); } if (--num[d[x]] == 0) break; num[d[x] = m + 1]++; cur[x] = 0; if (x != s) x = edges[p[x]].from; } } return flow; }};
Cherkassky B V, Goldberg A V. On implementing push-relabel method for the maximum flow problem[C]//International Conference on Integer Programming and Combinatorial Optimization. Springer, Berlin, Heidelberg, 1995: 157-171. ↩
Ahuja R K, Kodialam M, Mishra A K, et al. Computational investigations of maximum flow algorithms[J]. European Journal of Operational Research, 1997, 97(3): 509-542. ↩↩2↩3
Derigs U, Meier W. Implementing Goldberg’s max-flow-algorithm—A computational investigation[J]. Zeitschrift für Operations Research, 1989, 33(6): 383-403. ↩