Recent from talks
Nothing was collected or created yet.
Push–relabel maximum flow algorithm
View on WikipediaThis article needs editing to comply with Wikipedia's Manual of Style. In particular, it has problems with MOS:FORMULA - avoid mixing <math>...</math> and {{math}} in the same expression. (July 2025) |
In mathematical optimization, the push–relabel algorithm (alternatively, preflow–push algorithm) is an algorithm for computing maximum flows in a flow network. The name "push–relabel" comes from the two basic operations used in the algorithm. Throughout its execution, the algorithm maintains a "preflow" and gradually converts it into a maximum flow by moving flow locally between neighboring nodes using push operations under the guidance of an admissible network maintained by relabel operations. In comparison, the Ford–Fulkerson algorithm performs global augmentations that send flow following paths from the source all the way to the sink.[1]
The push–relabel algorithm is considered one of the most efficient maximum flow algorithms. The generic algorithm has a strongly polynomial O(V 2E) time complexity, which is asymptotically more efficient than the O(VE 2) Edmonds–Karp algorithm.[2] Specific variants of the algorithms achieve even lower time complexities. The variant based on the highest label node selection rule has O(V 2√E) time complexity and is generally regarded as the benchmark for maximum flow algorithms.[3][4] Subcubic O(VElog(V 2/E)) time complexity can be achieved using dynamic trees,[2] although in practice it is less efficient.[citation needed]
The push–relabel algorithm has been extended to compute minimum cost flows.[5] The idea of distance labels has led to a more efficient augmenting path algorithm, which in turn can be incorporated back into the push–relabel algorithm to create a variant with even higher empirical performance.[4][6]
History
[edit]The concept of a preflow was originally designed by Alexander V. Karzanov and was published in 1974 in Soviet Mathematical Dokladi 15. This pre-flow algorithm also used a push operation; however, it used distances in the auxiliary network to determine where to push the flow instead of a labeling system.[2][7]
The push-relabel algorithm was designed by Andrew V. Goldberg and Robert Tarjan. The algorithm was initially presented in November 1986 in STOC '86: Proceedings of the eighteenth annual ACM symposium on Theory of computing, and then officially in October 1988 as an article in the Journal of the ACM. Both papers detail a generic form of the algorithm terminating in O(V 2E) along with a O(V 3) sequential implementation, a O(VE log(V 2/E)) implementation using dynamic trees, and parallel/distributed implementation.[2][8] As explained in,[9] Goldberg-Tarjan introduced distance labels by incorporating them into the parallel maximum flow algorithm of Yossi Shiloach and Uzi Vishkin.[10]
Concepts
[edit]Definitions and notations
[edit]Let:
- G = (V, E) be a network with capacity function c: V × V → ∞,
- F = (G, c, s, t) a flow network, where s ∈ V and t ∈ V are chosen source and sink vertices respectively,
- f : V × V → denote a pre-flow in F,
- xf : V → denote the excess function with respect to the flow f, defined by xf (u) = Σv ∈ V f (v, u) − Σv ∈ V f (u, v),
- cf : V × V → ∞ denote the residual capacity function with respect to the flow f, defined by cf (e) = c(e) − f (e),
- Ef ⊂ E being the edges where f < c,
and
- Gf (V, Ef ) denote the residual network of G with respect to the flow f.
The push–relabel algorithm uses a nonnegative integer valid labeling function which makes use of distance labels, or heights, on nodes to determine which arcs should be selected for the push operation. This labeling function is denoted by 𝓁 : V → . This function must satisfy the following conditions in order to be considered valid:
- Valid labeling:
- 𝓁(u) ≤ 𝓁(v) + 1 for all (u, v) ∈ Ef
- Source condition:
- 𝓁(s) = | V |
- Sink conservation:
- 𝓁(t) = 0
In the algorithm, the label values of s and t are fixed. 𝓁(u) is a lower bound of the unweighted distance from u to t in Gf if t is reachable from u. If u has been disconnected from t, then 𝓁(u) − | V | is a lower bound of the unweighted distance from u to s. As a result, if a valid labeling function exists, there are no s-t paths in Gf because no such paths can be longer than | V | − 1.
An arc (u, v) ∈ Ef is called admissible if 𝓁(u) = 𝓁(v) + 1. The admissible network G̃f (V, Ẽf ) is composed of the set of arcs e ∈ Ef that are admissible. The admissible network is acyclic.
For a fixed flow f, a vertex v ∉ {s, t} is called active if it has positive excess with respect to f, i.e., xf (u) > 0.
Operations
[edit]Initialization
[edit]The algorithm starts by creating a residual graph, initializing the preflow values to zero and performing a set of saturating push operations on residual arcs (s, v) exiting the source, where v ∈ V \ {s}. Similarly, the labels are initialized such that the label at the source is the number of nodes in the graph, 𝓁(s) = | V |, and all other nodes are given a label of zero. Once the initialization is complete the algorithm repeatedly performs either the push or relabel operations against active nodes until no applicable operation can be performed.
Push
[edit]The push operation applies on an admissible out-arc (u, v) of an active node u in Gf. It moves min{xf (u), cf (u,v)} units of flow from u to v.
push(u, v):
assert xf[u] > 0 and 𝓁[u] == 𝓁[v] + 1
Δ = min(xf[u], c[u][v] - f[u][v])
f[u][v] += Δ
f[v][u] -= Δ
xf[u] -= Δ
xf[v] += Δ
A push operation that causes f (u, v) to reach c(u, v) is called a saturating push since it uses up all the available capacity of the residual arc. Otherwise, all of the excess at the node is pushed across the residual arc. This is called an unsaturating or non-saturating push.
Relabel
[edit]The relabel operation applies on an active node u which is neither the source nor the sink without any admissible out-arcs in Gf. It modifies 𝓁(u) to be the minimum value such that an admissible out-arc is created. Note that this always increases 𝓁(u) and never creates a steep arc, which is an arc (u, v) such that cf (u, v) > 0, and 𝓁(u) > 𝓁(v) + 1.
relabel(u):
assert xf[u] > 0 and 𝓁[u] <= 𝓁[v] for all v such that cf[u][v] > 0
𝓁[u] = 1 + min(𝓁[v] for all v such that cf[u][v] > 0)
Effects of push and relabel
[edit]After a push or relabel operation, 𝓁 remains a valid labeling function with respect to f.
For a push operation on an admissible arc (u, v), it may add an arc (v, u) to Ef, where 𝓁(v) = 𝓁(u) − 1 ≤ 𝓁(u) + 1; it may also remove the arc (u, v) from Ef, where it effectively removes the constraint 𝓁(u) ≤ 𝓁(v) + 1.
To see that a relabel operation on node u preserves the validity of 𝓁(u), notice that this is trivially guaranteed by definition for the out-arcs of u in Gf. For the in-arcs of u in Gf, the increased 𝓁(u) can only satisfy the constraints less tightly, not violate them.
The generic push–relabel algorithm
[edit]The generic push–relabel algorithm is used as a proof of concept only and does not contain implementation details on how to select an active node for the push and relabel operations. This generic version of the algorithm will terminate in O(V2E).
Since 𝓁(s) = | V |, 𝓁(t) = 0, and there are no paths longer than | V | − 1 in Gf, in order for 𝓁(s) to satisfy the valid labeling condition s must be disconnected from t. At initialisation, the algorithm fulfills this requirement by creating a pre-flow f that saturates all out-arcs of s, after which 𝓁(v) = 0 is trivially valid for all v ∈ V \ {s, t}. After initialisation, the algorithm repeatedly executes an applicable push or relabel operation until no such operations apply, at which point the pre-flow has been converted into a maximum flow.
generic-push-relabel(G, c, s, t):
create a pre-flow f that saturates all out-arcs of s
let 𝓁[s] = |V|
let 𝓁[v] = 0 for all v ∈ V \ {s}
while there is an applicable push or relabel operation do
execute the operation
Correctness
[edit]The algorithm maintains the condition that 𝓁 is a valid labeling during its execution. This can be proven true by examining the effects of the push and relabel operations on the label function 𝓁. The relabel operation increases the label value by the associated minimum plus one which will always satisfy the 𝓁(u) ≤ 𝓁(v) + 1 constraint. The push operation can send flow from u to v if 𝓁(u) = 𝓁(v) + 1. This may add (v, u) to Gf and may delete (u, v) from Gf . The addition of (v, u) to Gf will not affect the valid labeling since 𝓁(v) = 𝓁(u) − 1. The deletion of (u, v) from Gf removes the corresponding constraint since the valid labeling property 𝓁(u) ≤ 𝓁(v) + 1 only applies to residual arcs in Gf .[8]
If a preflow f and a valid labeling 𝓁 for f exists then there is no augmenting path from s to t in the residual graph Gf . This can be proven by contradiction based on inequalities which arise in the labeling function when supposing that an augmenting path does exist. If the algorithm terminates, then all nodes in V \ {s, t} are not active. This means all v ∈ V \ {s, t} have no excess flow, and with no excess the preflow f obeys the flow conservation constraint and can be considered a normal flow. This flow is the maximum flow according to the max-flow min-cut theorem since there is no augmenting path from s to t.[8]
Therefore, the algorithm will return the maximum flow upon termination.
Time complexity
[edit]In order to bound the time complexity of the algorithm, we must analyze the number of push and relabel operations which occur within the main loop. The numbers of relabel, saturating push and nonsaturating push operations are analyzed separately.
In the algorithm, the relabel operation can be performed at most (2| V | − 1)(| V | − 2) < 2| V |2 times. This is because the labeling 𝓁(u) value for any node u can never decrease, and the maximum label value is at most 2| V | − 1 for all nodes. This means the relabel operation could potentially be performed 2| V | − 1 times for all nodes V \ {s, t} (i.e. | V | − 2). This results in a bound of O(V 2) for the relabel operation.
Each saturating push on an admissible arc (u, v) removes the arc from Gf . For the arc to be reinserted into Gf for another saturating push, v must first be relabeled, followed by a push on the arc (v, u), then u must be relabeled. In the process, 𝓁(u) increases by at least two. Therefore, there are O(V) saturating pushes on (u, v), and the total number of saturating pushes is at most 2| V || E |. This results in a time bound of O(VE) for the saturating push operations.
Bounding the number of nonsaturating pushes can be achieved via a potential argument. We use the potential function Φ = Σ[u ∈ V ∧ xf (u) > 0] 𝓁(u) (i.e. Φ is the sum of the labels of all active nodes). It is obvious that Φ is 0 initially and stays nonnegative throughout the execution of the algorithm. Both relabels and saturating pushes can increase Φ. However, the value of Φ must be equal to 0 at termination since there cannot be any remaining active nodes at the end of the algorithm's execution. This means that over the execution of the algorithm, the nonsaturating pushes must make up the difference of the relabel and saturating push operations in order for Φ to terminate with a value of 0. The relabel operation can increase Φ by at most (2| V | − 1)(| V | − 2). A saturating push on (u, v) activates v if it was inactive before the push, increasing Φ by at most 2| V | − 1. Hence, the total contribution of all saturating pushes operations to Φ is at most (2| V | − 1)(2| V || E |). A nonsaturating push on (u, v) always deactivates u, but it can also activate v as in a saturating push. As a result, it decreases Φ by at least 𝓁(u) − 𝓁(v) = 1. Since relabels and saturating pushes increase Φ, the total number of nonsaturating pushes must make up the difference of (2| V | − 1)(| V | − 2) + (2| V | − 1)(2| V || E |) ≤ 4| V |2| E |. This results in a time bound of O(V 2E) for the nonsaturating push operations.
In sum, the algorithm executes O(V 2) relabels, O(VE) saturating pushes and O(V 2E) nonsaturating pushes. Data structures can be designed to pick and execute an applicable operation in O(1) time. Therefore, the time complexity of the algorithm is O(V 2E).[1][8]
Example
[edit]The following is a sample execution of the generic push-relabel algorithm, as defined above, on the following simple network flow graph diagram.
In the example, the h and e values denote the label 𝓁 and excess xf , respectively, of the node during the execution of the algorithm. Each residual graph in the example only contains the residual arcs with a capacity larger than zero. Each residual graph may contain multiple iterations of the perform operation loop.
The example (but with initial flow of 0) can be run here interactively.
Practical implementations
[edit]While the generic push–relabel algorithm has O(V 2E) time complexity, efficient implementations achieve O(V 3) or lower time complexity by enforcing appropriate rules in selecting applicable push and relabel operations. The empirical performance can be further improved by heuristics.
"Current-arc" data structure and discharge operation
[edit]The "current-arc" data structure is a mechanism for visiting the in- and out-neighbors of a node in the flow network in a static circular order. If a singly linked list of neighbors is created for a node, the data structure can be as simple as a pointer into the list that steps through the list and rewinds to the head when it runs off the end.
Based on the "current-arc" data structure, the discharge operation can be defined. A discharge operation applies on an active node and repeatedly pushes flow from the node until it becomes inactive, relabeling it as necessary to create admissible arcs in the process.
discharge(u):
while xf[u] > 0 do
if current-arc[u] has run off the end of neighbors[u] then
relabel(u)
rewind current-arc[u]
else
let (u, v) = current-arc[u]
if (u, v) is admissible then
push(u, v)
let current-arc[u] point to the next neighbor of u
Finding the next admissible edge to push on has amortized complexity. The current-arc pointer only moves to the next neighbor when the edge to the current neighbor is saturated or non-admissible, and neither of these two properties can change until the active node is relabelled. Therefore, when the pointer runs off, there are no admissible unsaturated edges and we have to relabel the active node , so having moved the pointer times is paid for by the relabel operation.[8]
Active node selection rules
[edit]Definition of the discharge operation reduces the push–relabel algorithm to repeatedly selecting an active node to discharge. Depending on the selection rule, the algorithm exhibits different time complexities. For the sake of brevity, we ignore s and t when referring to the nodes in the following discussion.
FIFO selection rule
[edit]The FIFO push–relabel algorithm[2] organizes the active nodes into a queue. The initial active nodes can be inserted in arbitrary order. The algorithm always removes the node at the front of the queue for discharging. Whenever an inactive node becomes active, it is appended to the back of the queue.
The algorithm has O(V 3) time complexity.
Relabel-to-front selection rule
[edit]The relabel-to-front push–relabel algorithm[1] organizes all nodes into a linked list and maintains the invariant that the list is topologically sorted with respect to the admissible network. The algorithm scans the list from front to back and performs a discharge operation on the current node if it is active. If the node is relabeled, it is moved to the front of the list, and the scan is restarted from the front.
The algorithm also has O(V 3) time complexity.
Highest label selection rule
[edit]The highest-label push–relabel algorithm[11] organizes all nodes into buckets indexed by their labels. The algorithm always selects an active node with the largest label to discharge.
The algorithm has O(V 2√E) time complexity. If the lowest-label selection rule is used instead, the time complexity becomes O(V 2E).[3]
Implementation techniques
[edit]Although in the description of the generic push–relabel algorithm above, 𝓁(u) is set to zero for each node u other than s and t at the beginning, it is preferable to perform a backward breadth-first search from t to compute exact labels.[2]
The algorithm is typically separated into two phases. Phase one computes a maximum pre-flow by discharging only active nodes whose labels are below n. Phase two converts the maximum preflow into a maximum flow by returning excess flow that cannot reach t to s. It can be shown that phase two has O(VE) time complexity regardless of the order of push and relabel operations and is therefore dominated by phase one. Alternatively, it can be implemented using flow decomposition.[9]
Heuristics are crucial to improving the empirical performance of the algorithm.[12] Two commonly used heuristics are the gap heuristic and the global relabeling heuristic.[2][13] The gap heuristic detects gaps in the labeling function. If there is a label 0 < 𝓁' < | V | for which there is no node u such that 𝓁(u) = 𝓁', then any node u with 𝓁' < 𝓁(u) < | V | has been disconnected from t and can be relabeled to (| V | + 1) immediately. The global relabeling heuristic periodically performs backward breadth-first search from t in Gf to compute the exact labels of the nodes. Both heuristics skip unhelpful relabel operations, which are a bottleneck of the algorithm and contribute to the ineffectiveness of dynamic trees.[4]
Sample implementations
[edit]#include <stdlib.h>
#include <stdio.h>
#define NODES 6
#define MIN(X,Y) ((X) < (Y) ? (X) : (Y))
#define INFINITE 10000000
void push(const int * const * C, int ** F, int *excess, int u, int v) {
int send = MIN(excess[u], C[u][v] - F[u][v]);
F[u][v] += send;
F[v][u] -= send;
excess[u] -= send;
excess[v] += send;
}
void relabel(const int * const * C, const int * const * F, int *height, int u) {
int v;
int min_height = INFINITE;
for (v = 0; v < NODES; v++) {
if (C[u][v] - F[u][v] > 0) {
min_height = MIN(min_height, height[v]);
height[u] = min_height + 1;
}
}
};
void discharge(const int * const * C, int ** F, int *excess, int *height, int *seen, int u) {
while (excess[u] > 0) {
if (seen[u] < NODES) {
int v = seen[u];
if ((C[u][v] - F[u][v] > 0) && (height[u] > height[v])) {
push(C, F, excess, u, v);
} else {
seen[u] += 1;
}
} else {
relabel(C, F, height, u);
seen[u] = 0;
}
}
}
void moveToFront(int i, int *A) {
int temp = A[i];
int n;
for (n = i; n > 0; n--) {
A[n] = A[n-1];
}
A[0] = temp;
}
int pushRelabel(const int * const * C, int ** F, int source, int sink) {
int *excess, *height, *list, *seen, i, p;
excess = (int *) calloc(NODES, sizeof(int));
height = (int *) calloc(NODES, sizeof(int));
seen = (int *) calloc(NODES, sizeof(int));
list = (int *) calloc((NODES-2), sizeof(int));
for (i = 0, p = 0; i < NODES; i++){
if ((i != source) && (i != sink)) {
list[p] = i;
p++;
}
}
height[source] = NODES;
excess[source] = INFINITE;
for (i = 0; i < NODES; i++)
push(C, F, excess, source, i);
p = 0;
while (p < NODES - 2) {
int u = list[p];
int old_height = height[u];
discharge(C, F, excess, height, seen, u);
if (height[u] > old_height) {
moveToFront(p, list);
p = 0;
} else {
p += 1;
}
}
int maxflow = 0;
for (i = 0; i < NODES; i++)
maxflow += F[source][i];
free(list);
free(seen);
free(height);
free(excess);
return maxflow;
}
void printMatrix(const int * const * M) {
int i, j;
for (i = 0; i < NODES; i++) {
for (j = 0; j < NODES; j++)
printf("%d\t",M[i][j]);
printf("\n");
}
}
int main(void) {
int **flow, **capacities, i;
flow = (int **) calloc(NODES, sizeof(int*));
capacities = (int **) calloc(NODES, sizeof(int*));
for (i = 0; i < NODES; i++) {
flow[i] = (int *) calloc(NODES, sizeof(int));
capacities[i] = (int *) calloc(NODES, sizeof(int));
}
// Sample graph
capacities[0][1] = 2;
capacities[0][2] = 9;
capacities[1][2] = 1;
capacities[1][3] = 0;
capacities[1][4] = 0;
capacities[2][4] = 7;
capacities[3][5] = 7;
capacities[4][5] = 4;
printf("Capacity:\n");
printMatrix(capacities);
printf("Max Flow:\n%d\n", pushRelabel(capacities, flow, 0, 5));
printf("Flows:\n");
printMatrix(flow);
return 0;
}
def relabel_to_front(C, source: int, sink: int) -> int:
n = len(C) # C is the capacity matrix
F = [[0] * n for _ in range(n)]
# residual capacity from u to v is C[u][v] - F[u][v]
height = [0] * n # height of node
excess = [0] * n # flow into node minus flow from node
seen = [0] * n # neighbours seen since last relabel
# node "queue"
nodelist = [i for i in range(n) if i != source and i != sink]
def push(u, v):
send = min(excess[u], C[u][v] - F[u][v])
F[u][v] += send
F[v][u] -= send
excess[u] -= send
excess[v] += send
def relabel(u):
# Find smallest new height making a push possible,
# if such a push is possible at all.
min_height = ∞
for v in range(n):
if C[u][v] - F[u][v] > 0:
min_height = min(min_height, height[v])
height[u] = min_height + 1
def discharge(u):
while excess[u] > 0:
if seen[u] < n: # check next neighbour
v = seen[u]
if C[u][v] - F[u][v] > 0 and height[u] > height[v]:
push(u, v)
else:
seen[u] += 1
else: # we have checked all neighbours. must relabel
relabel(u)
seen[u] = 0
height[source] = n # longest path from source to sink is less than n long
excess[source] = ∞ # send as much flow as possible to neighbours of source
for v in range(n):
push(source, v)
p = 0
while p < len(nodelist):
u = nodelist[p]
old_height = height[u]
discharge(u)
if height[u] > old_height:
nodelist.insert(0, nodelist.pop(p)) # move to front of list
p = 0 # start from front of list
else:
p += 1
return sum(F[source])
References
[edit]- ^ a b c Cormen, T. H.; Leiserson, C. E.; Rivest, R. L.; Stein, C. (2001). "§26 Maximum flow". Introduction to Algorithms (2nd ed.). The MIT Press. pp. 643–698. ISBN 978-0262032933.
- ^ a b c d e f g Goldberg, A V; Tarjan, R E (1986). "A new approach to the maximum flow problem". Proceedings of the eighteenth annual ACM symposium on Theory of computing – STOC '86. p. 136. doi:10.1145/12130.12144. ISBN 978-0897911931. S2CID 14492800.
- ^ a b Ahuja, Ravindra K.; Kodialam, Murali; Mishra, Ajay K.; Orlin, James B. (1997). "Computational investigations of maximum flow algorithms". European Journal of Operational Research. 97 (3): 509. CiteSeerX 10.1.1.297.2945. doi:10.1016/S0377-2217(96)00269-X.
- ^ a b c Goldberg, Andrew V. (2008). "The Partial Augment–Relabel Algorithm for the Maximum Flow Problem". Algorithms – ESA 2008. Lecture Notes in Computer Science. Vol. 5193. pp. 466–477. CiteSeerX 10.1.1.150.5103. doi:10.1007/978-3-540-87744-8_39. ISBN 978-3-540-87743-1.
- ^ Goldberg, Andrew V (1997). "An Efficient Implementation of a Scaling Minimum-Cost Flow Algorithm". Journal of Algorithms. 22: 1–29. doi:10.1006/jagm.1995.0805.
- ^ Ahuja, Ravindra K.; Orlin, James B. (1991). "Distance-directed augmenting path algorithms for maximum flow and parametric maximum flow problems". Naval Research Logistics. 38 (3): 413. CiteSeerX 10.1.1.297.5698. doi:10.1002/1520-6750(199106)38:3<413::AID-NAV3220380310>3.0.CO;2-J.
- ^ Goldberg, Andrew V.; Tarjan, Robert E. (2014). "Efficient maximum flow algorithms". Communications of the ACM. 57 (8): 82. doi:10.1145/2628036. S2CID 17014879.
- ^ a b c d e Goldberg, Andrew V.; Tarjan, Robert E. (1988). "A new approach to the maximum-flow problem". Journal of the ACM. 35 (4): 921. doi:10.1145/48014.61051. S2CID 52152408.
- ^ a b Ahuja, R. K.; Magnanti, T. L.; Orlin, J. B. (1993). Network Flows: Theory, Algorithms, and Applications (1st ed.). Prentice Hall. ISBN 978-0136175490.
- ^ Shiloach, Yossi; Vishkin, Uzi (1982). "An O(n2log n) parallel max-flow algorithm". Journal of Algorithms. 3 (2): 128–146. doi:10.1016/0196-6774(82)90013-X.
- ^ Cheriyan, J.; Maheshwari, S. N. (1988). "Analysis of preflow push algorithms for maximum network flow". Foundations of Software Technology and Theoretical Computer Science. Lecture Notes in Computer Science. Vol. 338. p. 30. doi:10.1007/3-540-50517-2_69. ISBN 978-3-540-50517-4.
- ^ Cherkassky, Boris V.; Goldberg, Andrew V. (1995). "On implementing push-relabel method for the maximum flow problem". Integer Programming and Combinatorial Optimization. Lecture Notes in Computer Science. Vol. 920. p. 157. CiteSeerX 10.1.1.150.3609. doi:10.1007/3-540-59408-6_49. ISBN 978-3-540-59408-6.
- ^ Derigs, U.; Meier, W. (1989). "Implementing Goldberg's max-flow-algorithm ? A computational investigation". Zeitschrift für Operations Research. 33 (6): 383. doi:10.1007/BF01415937. S2CID 39730584.
Push–relabel maximum flow algorithm
View on GrokipediaIntroduction
Algorithm overview
The push–relabel algorithm, also known as the preflow–push algorithm, is a method for computing the maximum flow in a flow network by maintaining a preflow—a relaxation of a valid flow that allows excess flow at intermediate nodes—and using node labels to direct adjustments toward the sink.[3] Introduced as an alternative to traditional augmenting path approaches, it initializes a preflow by saturating edges from the source and then iteratively refines it without requiring global path searches.[3] At its core, the algorithm identifies nodes with excess flow and pushes portions of this excess along admissible edges, defined by the current labels, to neighboring nodes closer to the sink.[3] When no admissible edges are available from a node with excess, the algorithm relabels the node by increasing its label to approximate the shortest path distance to the sink in the residual graph, thereby creating new admissible opportunities for pushing.[3] This local adjustment process continues until all excesses are resolved, yielding a maximum flow.[3] Unlike augmenting path methods, the push–relabel algorithm operates in-place on the network structure, updating flows and labels directly without explicitly constructing or traversing full paths from source to sink in each iteration.[3] This design avoids the overhead of repeated path-finding, making it particularly efficient for networks where such searches would be costly.[3] In practice, the algorithm often outperforms path-based methods like Ford–Fulkerson on dense graphs, achieving a time complexity of O(n³) for n-vertex networks in its basic form, due to its ability to handle local operations scalably.[3]Relation to other maximum flow algorithms
The Ford–Fulkerson algorithm establishes the foundational approach to computing maximum flows by iteratively identifying augmenting paths in the residual graph and augmenting the flow along them until saturation, providing a flexible framework but without guaranteed polynomial runtime in general cases.[4] The Edmonds–Karp implementation refines this method by employing breadth-first search to consistently select shortest augmenting paths, yielding polynomial-time performance and particular efficiency on sparse graphs where path lengths remain manageable. Dinic's algorithm builds on the augmenting path strategy by introducing level graphs—via breadth-first search from the source—to partition the network and compute blocking flows within each level, enabling multiple augmentations per phase and improving overall efficiency across varied network structures.[3] Unlike these path-based methods, the push–relabel algorithm shifts to a preflow framework, where an initial feasible preflow is established, and excess flow is locally redistributed through push operations along admissible edges or relabel operations to update distance labels, avoiding global path searches and often achieving superior practical performance.[3] Push–relabel particularly excels in dense networks, where its structure supports rapid excess propagation without the overhead of repeated path finding, whereas Edmonds–Karp demonstrates greater efficiency in sparse graphs due to its BFS-guided augmentations that minimize path exploration costs.[3] Empirical studies confirm that preflow-push variants, including push–relabel, generally outperform augmenting path algorithms like Dinic's and Edmonds–Karp across diverse problem classes.[5]History
Origins and development
The push–relabel maximum flow algorithm was introduced in 1986 by Andrew V. Goldberg and Robert E. Tarjan as a novel approach to computing maximum flows in directed graphs with capacities, building on the preflow concept originally developed by A. V. Karzanov in 1974.[3] Their method sought to address the limitations of traditional path-augmenting algorithms, such as those by Ford and Fulkerson or Dinic, which often perform poorly on large-scale networks due to the repeated search for augmenting paths that can become computationally expensive as the flow increases.[3] This work connected to earlier precursors in flow computation, notably the 1979 algorithm by Alon Itai and Yossi Shiloach for maximum flows in planar networks, which employed level-based pushing of flow excesses without full path augmentation, laying groundwork for local flow adjustments.[6] Itai and Shiloach's technique highlighted the potential of non-path-based strategies for efficiency in restricted graph classes, influencing the generalization to arbitrary networks in the push–relabel framework.[6] The algorithm was first presented at the 18th Annual ACM Symposium on Theory of Computing in 1986 and formally published in the Journal of the ACM in 1988, where Goldberg and Tarjan established a time complexity of for the generic algorithm, with a highest-label variant achieving for dense graphs, demonstrating polynomial-time solvability without relying on augmenting paths.[3] This bound marked a significant theoretical advancement, particularly for dense graphs.[3]Key contributors and publications
The push–relabel maximum flow algorithm was primarily developed by Andrew V. Goldberg during his PhD research at MIT, in collaboration with Robert E. Tarjan at Princeton, with the foundational ideas emerging from Goldberg's 1987 dissertation and culminating in their collaborative publication. The core algorithm was formally presented in the 1988 paper "A New Approach to the Maximum-Flow Problem," published in the Journal of the Association for Computing Machinery, where they introduced the preflow-push framework, basic operations, and initial time bounds including O(V^3) for a highest-label variant. In the 1980s, contributions from researchers such as Harold N. Gabow advanced the broader field of maximum flow algorithms through refined data structures and scaling methods, which influenced efficient implementations of push–relabel and related techniques. Gabow's 1985 paper "Scaling Algorithms for Network Problems" provided key insights into parameter scaling for network optimization, enabling faster handling of capacities and supporting subsequent refinements in flow computation. Early extensions focused on practical heuristics, notably the gap relabeling technique introduced by U. Derigs and W. Meier in their 1989 computational study "Implementing Goldberg's Max-Flow Algorithm—A Computational Investigation." This heuristic detects gaps in label values to accelerate relabeling, significantly improving empirical performance without altering the theoretical foundations. Subsequent works in the 1990s refined selection rules and implementations for better practicality. Boris V. Cherkassky and Andrew V. Goldberg's 1995 paper "On Implementing Push–Relabel Method for the Maximum Flow Problem," presented at the International Conference on Integer Programming and Combinatorial Optimization, analyzed operation orderings like highest-label first, leading to robust codes that achieved the O(V^3) bound in practice while outperforming prior variants on large instances.[7] This publication emphasized the algorithm's evolution toward efficient, heuristic-driven versions suitable for real-world networks. The development and impact of push–relabel are comprehensively surveyed in the 1993 textbook Network Flows: Theory, Algorithms, and Applications by Ravindra K. Ahuja, Thomas L. Magnanti, and James B. Orlin, which dedicates a chapter to the algorithm, its variants, and 1990s advancements achieving improved practical speeds through combined heuristics and data structures. By the mid-1990s, these efforts had established push–relabel as a cornerstone method, with O(V^3) implementations and heuristic enhancements making it competitive for dense graphs up to thousands of vertices.[7]Core Concepts
Definitions and notation
The push–relabel algorithm operates on a flow network, which is a directed graph consisting of a set of vertices and a set of directed edges , with a distinguished source vertex and sink vertex . Each edge is assigned a nonnegative real-valued capacity , while if . A preflow in this network is a real-valued function that satisfies two conditions: the capacity constraint for all , and skew symmetry for all . Unlike a standard flow, a preflow does not necessarily satisfy the flow conservation property at all vertices except and ; instead, it allows for excess flow accumulation at intermediate vertices. The excess at a vertex under preflow is defined as , representing the net flow into . For all , a valid preflow requires . The residual graph with respect to a preflow has the same vertex set but an edge set , where the residual capacity is given by . This formulation accounts for both forward edges and potential backward edges induced by the skew-symmetric flow. Standard notation for the algorithm includes for the number of vertices and for the number of edges in the original network. Additionally, denotes the distance label (or height) assigned to each vertex .Labels, excesses, and distances
In the push–relabel algorithm, the excess at a vertex , denoted , quantifies the imbalance of flow at and is defined as the net flow into : , where is the current preflow and is the set of vertices.[3] This value is zero for a valid flow satisfying conservation at intermediate vertices but positive for excess flow at non-sink vertices in the preflow used by the algorithm; at the source, it is negative.[3] The excess plays a central role in tracking deviations from flow conservation, with the algorithm aiming to reduce all positive excesses to zero except at the sink.[3] Each vertex is assigned a label , a non-negative integer that serves as an estimate of the shortest-path distance from to the sink in the residual graph.[3] Initially, the labels are set such that , where is the source and is the number of vertices, and for all other vertices .[3] These initial labels provide a conservative starting point, ensuring is sufficiently large to bound potential distance increases while keeping other labels minimal.[3] An admissible edge in the residual graph is a directed edge with positive residual capacity such that .[3] This condition enforces a strict decrease in labels along the edge, guiding flow pushes toward the sink along paths that mimic shortest paths in terms of label distances.[3] Admissible edges thus restrict operations to "downhill" directions in the label landscape, preventing cycles and promoting progress toward resolving excesses.[3] The algorithm maintains a validity condition on the labels to ensure correctness: for every vertex with , , where denotes the edges in the residual graph.[3] This guarantees that any vertex holding excess has at least one outgoing residual edge to a vertex with a label exactly one less, allowing potential reduction of the excess while preserving the overall label structure.[3] The condition, combined with the global labeling property that for all residual edges , upholds the estimates' reliability throughout execution.[3]Basic Operations
Initialization
The initialization phase of the push–relabel algorithm establishes an initial preflow and distance labels to prepare the flow network for subsequent operations. The flow function is set to zero on all edges, meaning for every edge in the network, ensuring no flow traverses the graph initially.[3] Distance labels are then assigned as follows: the source vertex receives label , where is the total number of vertices in the network, while all other vertices are assigned . This setup positions the source at a maximal distance, reflecting its role as the origin of flow, and initializes other nodes at the minimum distance level.[3] To create an initial preflow, all outgoing edges from the source are saturated by setting for each neighbor of , where denotes the capacity of edge ; correspondingly, the reverse edges receive to maintain antisymmetry in the residual graph. This saturation introduces excesses (defined as the net inflow imbalance at a node, as detailed in the section on labels, excesses, and distances) solely at the vertices adjacent to the source, with for those , while the sink has no excess and no flow has yet reached it. The resulting structure is a valid preflow with all excess concentrated near the source, setting the stage for flow propagation without violating capacity constraints.[3]Push operation
The push operation in the push–relabel algorithm is the primary mechanism for redistributing excess flow from an active vertex to one of its admissible neighbors in the residual graph, thereby progressing toward a valid maximum flow without violating network constraints. An admissible edge from vertex to vertex exists when the residual capacity and the distance labels satisfy . This operation is applicable when vertex (distinct from the source and sink ) is active, meaning it has positive excess and a finite distance label , and at least one admissible neighbor is available with . Upon selecting such a neighbor , the algorithm determines the push amount as the minimum of the current excess at and the residual capacity along the edge: This ensures that the push neither exhausts the excess at beyond zero nor saturates the edge prematurely if limited by capacity. The flow is then updated by augmenting units along the edge: and , where represents the flow on the reverse edge (potentially introducing negative flow to maintain antisymmetry). Simultaneously, the excesses are recalculated: and . As a result, the push reduces the excess at the active vertex while transferring it to , preserving the overall preflow properties—such as non-negative excesses, capacity bounds, and flow antisymmetry—along with the validity of the distance labeling, where labels remain valid estimates that underestimate the shortest-path distances to the sink in the residual graph. This step maintains algorithmic progress by decreasing local imbalances without altering the total excess in the network.[3]Relabel operation
The relabel operation in the push–relabel algorithm is performed on an active node (i.e., a node distinct from the source and sink with excess ) when no admissible outgoing residual edges exist, meaning there is no neighbor such that the residual capacity and the label satisfies . This condition arises after repeated push operations have saturated all admissible edges from , preventing further flow discharge via pushes.[3] To execute the relabel, the algorithm updates the label of to the smallest possible value that maintains label validity: where denotes the residual edges; if no such exists, then , with representing infinity in this context. This computation scans the outgoing residual edges of to find the minimum over neighboring labels plus one, ensuring the new is a lower bound on the shortest-path distance from to the sink in the residual graph .[3] The relabel operation preserves the validity of labels as distance estimates, where for any node with , underestimates the distance to , and edges in satisfy . Critically, each relabel strictly increases by at least 1, as the previous label was already the minimum possible before the update, which bounds the number of relabels per node to and the total across all nodes to . This monotonic increase unblocks the node by potentially creating new admissible edges in subsequent operations, allowing the algorithm to progress toward excess elimination at the sink.[3]Generic Algorithm
Algorithm description and pseudocode
The push–relabel algorithm, also known as the preflow-push algorithm, computes the maximum flow in a capacitated directed graph by maintaining a preflow—a relaxation of a flow that allows excess flow at vertices—and using distance labels to estimate shortest paths in the residual graph, guiding saturating pushes of excess toward the sink.[1] The algorithm proceeds in two phases: initialization, which saturates all outgoing edges from the source to create initial excesses at neighboring vertices, and a refinement phase that iteratively reduces excesses at non-source, non-sink vertices through local operations until the preflow becomes a valid maximum flow.[1] Initialization sets the preflow such that and for all edges from the source , with otherwise; distance labels are set to (where is the number of vertices) and for all other vertices ; excesses are then computed as the net inflow minus outflow at each .[1] The refinement phase operates in a loop: while there exists an active vertex (i.e., , , and ), select such a and invoke the discharge procedure on it.[1] The discharge procedure for repeatedly applies the push operation to an admissible residual neighbor (where residual capacity and ) if one exists, transferring units of flow and updating excesses and the preflow; otherwise, it applies the relabel operation, setting to (or if no such exists), until or .[1] These push and relabel operations, detailed in prior sections, ensure excess is propagated downhill along estimated shortest paths in the residual graph.[1] The algorithm terminates when no active vertices remain, at which point the preflow is a maximum flow, with value equal to the net outflow from the source, .[1] The following pseudocode outlines the generic push–relabel algorithm, based on the original presentation.[1]procedure Initialize(V, E, s, t, c)
for each vertex v ≠ s
d(v) ← 0
d(s) ← |V|
for each edge (u, v) ∈ E
f(u, v) ← 0
f(v, u) ← 0
for each edge (s, v) ∈ E
f(s, v) ← c(s, v)
f(v, s) ← -c(s, v)
for each vertex v ∈ V
e(v) ← ∑_{u:(u,v)∈E} f(u, v) - ∑_{w:(v,w)∈E} f(v, w)
procedure Push(v, w)
δ ← min(e(v), r_f(v, w))
f(v, w) ← f(v, w) + δ
f(w, v) ← f(w, v) - δ
e(v) ← e(v) - δ
e(w) ← e(w) + δ
procedure Relabel(v)
if there exists w such that (v, w) ∈ E_f
d(v) ← min { d(w) + 1 | (v, w) ∈ E_f }
else
d(v) ← |V|
procedure Discharge(v)
while e(v) > 0 and d(v) < |V|
if exists w such that (v, w) ∈ E_f and r_f(v, w) > 0 and d(v) = d(w) + 1
Push(v, w)
else
Relabel(v)
procedure GenericPushRelabel(V, E, s, t, c)
Initialize(V, E, s, t, c)
while exists v ≠ s, t with e(v) > 0 and d(v) < |V|
select such a v
Discharge(v)
return f // now a maximum flow
procedure Initialize(V, E, s, t, c)
for each vertex v ≠ s
d(v) ← 0
d(s) ← |V|
for each edge (u, v) ∈ E
f(u, v) ← 0
f(v, u) ← 0
for each edge (s, v) ∈ E
f(s, v) ← c(s, v)
f(v, s) ← -c(s, v)
for each vertex v ∈ V
e(v) ← ∑_{u:(u,v)∈E} f(u, v) - ∑_{w:(v,w)∈E} f(v, w)
procedure Push(v, w)
δ ← min(e(v), r_f(v, w))
f(v, w) ← f(v, w) + δ
f(w, v) ← f(w, v) - δ
e(v) ← e(v) - δ
e(w) ← e(w) + δ
procedure Relabel(v)
if there exists w such that (v, w) ∈ E_f
d(v) ← min { d(w) + 1 | (v, w) ∈ E_f }
else
d(v) ← |V|
procedure Discharge(v)
while e(v) > 0 and d(v) < |V|
if exists w such that (v, w) ∈ E_f and r_f(v, w) > 0 and d(v) = d(w) + 1
Push(v, w)
else
Relabel(v)
procedure GenericPushRelabel(V, E, s, t, c)
Initialize(V, E, s, t, c)
while exists v ≠ s, t with e(v) > 0 and d(v) < |V|
select such a v
Discharge(v)
return f // now a maximum flow
Correctness
The push–relabel algorithm maintains the invariant that the current flow is always a preflow, meaning it respects edge capacities ( for all edges) and satisfies the excess condition ( for all vertices ).[3] This property holds initially after setting the preflow from the source and is preserved by the push and relabel operations, as pushes only occur along admissible residual edges without violating capacities, and relabels do not alter the flow.[3] A second key invariant ensures the labels are feasible: for every vertex with , the label satisfies , where is the shortest-path distance from to the sink in the residual graph , and admissible edges (residual edges where ) respect the labeling condition .[3] This feasibility is established by the initial labeling (with and for , except ) and maintained through operations, as pushes preserve shortest-path distances for zero-excess vertices and relabels update labels to values ≤ actual distances in the residual graph.[3] The algorithm terminates because labels are bounded above by (where is the number of vertices), each relabel operation strictly increases the label of some vertex, and the total number of such operations across all vertices is at most , leading to an overall bound of operations (with edges), after which no active vertices (those with and ) remain.[3] Upon termination, the preflow is a maximum flow because no augmenting path exists from to in : if such a path existed, its vertices would form a sequence with non-increasing labels (due to the admissible edge condition), contradicting the feasibility invariant and the absence of active vertices that could initiate a push sequence toward .[3] Moreover, the value of the flow equals the capacity of the minimum - cut defined by the sets and (with , ), as no residual edges cross from to (labels in are labels in ; by the labeling lemma for residual , any such edge would require , , but at termination no admissible edges exist from vertices in ), and all excess has been pushed to .[3]Time complexity
The time complexity of the generic push–relabel algorithm is determined by bounding the number of push and relabel operations executed during its run. Each operation is assumed to take constant time when implemented with appropriate data structures, such as adjacency lists for examining admissible edges. Saturating pushes, which fill an edge to its residual capacity, occur at most times. This bound arises because each such push reduces the total excess flow in the network by at least 1 unit, and the initial total excess introduced during initialization is bounded by in the worst case, considering the structure of the flow network with vertices and edges. Non-saturating pushes, which partially reduce excess without saturating an edge, are bounded by . This tighter analysis employs a potential function , where is the distance label of vertex . Each non-saturating push either decreases the excess at a vertex or increases by at most 1, while relabels can increase by at most . Since starts at 0 and is bounded above by , the total number of such pushes across all vertices is limited to , as each push examines edges incident to a vertex. Relabel operations, which update a vertex's distance label to reflect the shortest path estimate to the sink, occur at most times in total. For each vertex, the label is non-decreasing and increases by at least 1 each time it is relabeled, with the maximum possible value being ; thus, each of the vertices (excluding source and sink) can be relabeled at most times. Combining these bounds, the overall time complexity of the generic algorithm is , dominated by the non-saturating pushes. For networks with unit capacities on all edges, the analysis simplifies further, yielding an improved bound of , as the reduced excess propagation limits the number of operations per vertex.Example
Step-by-step walkthrough
Consider a simple flow network with nodes (source), , , and (sink). The directed edges and their capacities are: with capacity 4, with capacity 3, with capacity 2, with capacity 3, and with capacity 2. The maximum flow in this network is 5. Initialization. Saturate the outgoing edges from the source to establish the initial preflow: set flow on to 4 and on to 3. This yields excesses and . The distance labels (heights) are set to , . The active nodes (with positive excess, excluding and ) are and . Residual capacities are updated accordingly: , ; , ; all other forward residuals remain at their capacities, with backward residuals at 0. Select active node for discharge (while ). With , no admissible edges exist (requiring a neighbor with and positive residual capacity). Relabel : the neighbors with positive residual are (), (), and (); the minimum , so . Now admissible edges are to and (both , residuals 2 and 3). Select edge for push: . Push 3 units: flow , , update residuals , . Still ; next admissible edge is : . Push 1 unit: flow , , ; residuals , . Node is now inactive. Select active node for discharge (while ). With , no admissible edges. Relabel : neighbors with positive residual are (, ), (, ), and (, ); minimum , so . Now admissible edge is to (): . Push 2 units: flow , ; residuals , . Still , but no remaining admissible edges. Relabel : neighbors now (), (), (residual 0); minimum , so . Admissible edge is to (, ): . Push 1 unit along backward edge : reduce flow to 0, , ; residuals , . Still , no admissible edges. Relabel : only (, residual 3); . Admissible edge to (): . Push 1 unit along backward : reduce flow to 2, ; residuals , . Node is inactive. Now the only active node is (, ). No admissible edges (to , to residual 0). Relabel : neighbors (, ), (, ); minimum , so . Now admissible edge to (): . Push 1 unit along backward : reduce flow to 3, ; residuals , . No active nodes remain. The final flow values are: , , , , . The value of the maximum flow is 5 (total outflow from ). The residual graph has: , ; , ; , ; , ; , . All intermediate excesses are zero, confirming a valid maximum flow.| Edge | Original Capacity | Final Flow | Final Residual Forward | Final Residual Backward |
|---|---|---|---|---|
| s → a | 4 | 3 | 1 | 3 |
| s → b | 3 | 2 | 1 | 2 |
| a → b | 2 | 0 | 2 | 0 |
| a → t | 3 | 3 | 0 | 3 |
| b → t | 2 | 2 | 0 | 2 |
Flow network illustration
A standard illustrative example for the push–relabel algorithm uses a simple flow network with four nodes: source , intermediate nodes and , and sink . The edges and their capacities are as follows: with capacity 1, with capacity 100, with capacity 100, with capacity 1, and with capacity 1.[8] The initial network diagram can be visualized as: s
/ \
1 100
v w
| \ |
1 100 1
| \ |
t t
s
/ \
1 100
v w
| \ |
1 100 1
| \ |
t t
Practical Implementations
Data structures and discharge
The push–relabel algorithm relies on carefully designed data structures to implement push and relabel operations efficiently, ensuring that the overall time complexity remains practical despite potentially many iterations. The residual graph is typically represented using adjacency lists, where each vertex maintains a list of its outgoing residual edges along with associated capacities and current flow values. This structure allows O(1) access to residual capacities for potential pushes and supports dynamic updates as flow changes, using O(m) space where m is the number of edges.[9][10] To avoid repeatedly scanning all outgoing edges from a vertex during discharge, the current-arc structure is employed. Each vertex stores a pointer to its "current arc," which is the last edge checked for a push operation, initializing to the first edge in the adjacency list. When attempting a push from vertex u, the algorithm starts from this current arc and advances the pointer only when no admissible push is possible, effectively amortizing the edge traversal cost over multiple operations and limiting full list scans to O(n) times per relabeling phase per vertex. This technique, analyzed to contribute O(nm) total time across all pushes, is crucial for implementations achieving O(n^3) or better bounds.[9][2] The discharge operation integrates pushes and relabels for a single active vertex u (with excess e(u) > 0) in a loop until e(u) = 0 or no further progress is possible, systematically reducing excess by repeatedly selecting the current admissible edge for a push if the neighbor's label is strictly lower, or advancing the current arc otherwise. If all edges are exhausted without reducing excess, a relabel occurs, resetting the current arc to the first edge. This combined procedure ensures that each discharge fully processes one vertex before moving on, with the following simplified pseudocode illustrating its structure:procedure discharge(u):
while e(u) > 0:
if current_arc(u) has an admissible neighbor v ([label](/page/Label)(u) > [label](/page/Label)(v) and c_f(u,v) > 0):
push(u, v)
else:
advance current_arc(u)
if no more edges from current_arc(u):
relabel(u)
reset current_arc(u) to first edge
procedure discharge(u):
while e(u) > 0:
if current_arc(u) has an admissible neighbor v ([label](/page/Label)(u) > [label](/page/Label)(v) and c_f(u,v) > 0):
push(u, v)
else:
advance current_arc(u)
if no more edges from current_arc(u):
relabel(u)
reset current_arc(u) to first edge
Node selection rules
In the push–relabel algorithm, node selection determines which active node (one with positive excess flow) is chosen for discharge next, a choice that does not affect the algorithm's correctness but significantly influences practical efficiency by minimizing the number of relabel operations and optimizing the sequence of pushes.[3] The generic rule allows selection of any active node, ensuring the preflow remains valid while progressing toward a maximum flow, though arbitrary choices can lead to excessive relabels and higher computational cost. Common heuristics prioritize nodes to reduce total work: the FIFO (first-in, first-out) rule maintains a queue of active nodes and processes them in the order they become active, promoting balanced workload distribution and often achieving O(nm) time complexity in practice, where n is the number of nodes and m is the number of edges. The highest-label rule selects the active node with the maximum distance label, focusing pushes on nodes closest to the sink and bounding nonsaturating pushes at O(n²√m), which enhances convergence and is particularly effective for dense networks.[11] The relabel-to-front heuristic, when a node is relabeled, moves it to the front of the selection list (often combined with FIFO), improving cache efficiency by reusing recently updated nodes and further reducing iterations. These rules impact performance by improving cache locality—through sequential access in FIFO or localized updates in relabel-to-front—and distributing work to avoid bottlenecks, with empirical studies showing that well-chosen selections can yield near-linear running times on real-world networks despite worst-case bounds.[11]Optimization techniques
Several optimization techniques have been developed to enhance the practical performance of the push–relabel algorithm, addressing issues such as inflated distance labels that lead to inefficient relabel operations. These refinements focus on reducing the number of relabels and improving the efficiency of finding admissible edges, often at the cost of additional preprocessing or data structure maintenance. Seminal implementations incorporate multiple such heuristics to achieve runtimes significantly better than the generic version's worst-case bounds.[9] Gap relabeling is a heuristic that detects and skips "gaps" in the distance labels, where no active nodes exist at certain label values between 0 and (the number of vertices). When a gap is identified—such as no nodes with label where —all nodes with labels greater than or equal to are immediately relabeled to , marking them as unreachable from the sink and deactivating them without further processing. This avoids unnecessary relabel attempts on disconnected nodes and was independently proposed by Cherkassky and by Derigs and Meier.[9] The technique can be implemented efficiently using a linked list or array to track nodes by label, with low overhead per detection, and it significantly improves runtime by ensuring most work on gaps is useful. In combination with other heuristics, gap relabeling contributes to a practical time complexity of for dense graphs.[9] Global relabeling periodically recomputes exact shortest-path distances from all nodes to the sink in the current residual graph using a backward breadth-first search (BFS), resetting potentially inflated labels to their true values. This is performed after a fixed number of relabels, such as every operations, to prevent label values from growing excessively and causing redundant pushes. The BFS takes time per invocation, making it computationally intensive but effective in reducing the total number of relabels over the algorithm's execution. Introduced as a key refinement in efficient implementations, global relabeling complements gap relabeling by addressing broader label inaccuracies and has been shown to drastically improve practical performance on large instances.[9] For networks with unit capacities (where all edge capacities are 1), optimizations treat such edges specially to accelerate push operations, as saturating a unit-capacity edge immediately removes it from consideration in the residual graph. In these "simple" networks, pushes on unit edges are simplified since the excess is typically small (at most 1 per node after initialization), and relabels are bounded more tightly due to the limited possible label increases. This leads to an overall time complexity of for the optimized push–relabel algorithm on unit-capacity graphs, making it particularly efficient for bipartite matching and similar problems. Dynamic trees provide a sophisticated data structure for modern implementations, enabling faster discovery of admissible edges by maintaining a forest representation of the residual graph where nodes are grouped by distance labels. Introduced by Goldberg and Tarjan, these link-cut trees support push and relabel operations in amortized time per action by efficiently finding children in the tree structure and updating paths during relabels. Post-2000 refinements have integrated dynamic trees with parallel push–relabel variants, further improving scalability on multicore systems while preserving the bound for general graphs. Recent advancements as of 2025 include GPU-accelerated implementations using two-level parallelism and efficient CSR designs, achieving up to 2× speedups over classical versions, particularly for large-scale graphs.[12] Warm-starting techniques allow initializing with predicted flows, providing theoretical guarantees for faster convergence in machine learning-augmented settings.[13] Additionally, hybrid approaches combining push-relabel with augmenting paths have improved theoretical bounds to time.[14]Comparisons and Applications
Comparison with Ford–Fulkerson and Dinic's algorithms
The Ford–Fulkerson method, in its breadth-first search implementation known as Edmonds–Karp, achieves a time complexity of O(VE²) in the worst case, where V is the number of vertices and E is the number of edges, by repeatedly finding shortest augmenting paths until no more exist. This approach is conceptually simple and guarantees termination due to the monotonic increase in path lengths, but it performs poorly on graphs with large capacities or dense structures, as it may require many augmentations proportional to the maximum flow value. In contrast, the push–relabel algorithm operates on a preflow rather than augmenting paths, achieving O(V²E) time complexity in its generic form and demonstrating superior performance on dense graphs, where augmenting path methods like Edmonds–Karp scale quadratically with edge density.[15][3] Dinic's algorithm, a blocking-flow method, refines the augmenting path paradigm by constructing level graphs in phases and finding multiple blocking flows per phase, yielding a time complexity of O(V²E) for general capacities and O(\sqrt{V} E) for unit-capacity networks. While theoretically competitive, especially for unit capacities where it outperforms push–relabel's O(V³) bound in such cases, Dinic's requires global operations like level graph construction, which can be computationally intensive. Push–relabel, relying on local push and relabel operations, often executes faster in practice across diverse graph classes, as evidenced by computational studies showing preflow-push variants substantially outperforming Dinic's on DIMACS challenge instances.[15][5] Empirical benchmarks highlight push–relabel's advantages on large-scale instances; for example, optimized variants solve maximum flow on transportation-derived bipartite graphs with up to 300,000 nodes and millions of edges in seconds to minutes, far exceeding the practical limits of Edmonds–Karp on similar sizes.[16] In DIMACS implementations, highest-label push–relabel variants process graphs with 10,000 vertices more efficiently than Dinic's or augmenting-path methods, particularly in dense or real-world network scenarios like assignment problems.[17][5] Key trade-offs include push–relabel's suitability for parallelization due to its vertex-centric local operations, enabling efficient distribution across processors without global synchronization, unlike Dinic's phase-based structure. Conversely, Dinic's level-graph traversals can be more cache-friendly on sparse graphs by minimizing random memory access, though this benefit diminishes in dense settings where push–relabel's heuristics shine.[5] Overall, while all three algorithms are polynomial-time, push–relabel strikes a balance for practical deployment in large, irregular networks.[15]Real-world applications and performance
The push–relabel algorithm finds application in network routing, where it models data packet flows through communication networks to maximize throughput while respecting capacity constraints.[18] In airline scheduling, it optimizes seat allocations and route assignments by treating airports and flights as nodes and edges in a flow network.[18] Job scheduling systems employ it to allocate resources across tasks, ensuring efficient distribution without exceeding processor limits.[18] Additionally, in computer vision, the algorithm supports image segmentation through graph cuts, where pixels are nodes and similarities define edge capacities, enabling precise object boundary detection via min-cut computation.[19] In Google's online advertising platform, push–relabel variants solve maximum flow problems on large bipartite graphs to manage ad inventory reservations, processing availability queries for bookable impressions across millions of nodes and edges.[16] The algorithm's implementations appear in established libraries such as the Boost Graph Library, which provides the push_relabel_max_flow function for general flow networks, and LEMON, an open-source C++ template library offering optimized push–relabel routines with heuristics for enhanced efficiency.[20][21] Empirical benchmarks on real-world bipartite graphs from advertising systems demonstrate that bi-directional FIFO and excess-scaling variants of push–relabel outperform general implementations by 3–10 times in execution time and operations, handling graphs with up to 300,000 nodes and 2.5 million edges in seconds to minutes.[16] Recent 2020s advancements include GPU-accelerated versions; for instance, a 2020 dynamic push–relabel implementation on NVIDIA Tesla V100 processes static max-flow on large social network graphs (e.g., 1.6 million vertices, 30.6 million edges) with optimized global relabeling, outperforming prior CPU baselines by orders of magnitude in throughput.[22] Recent theoretical advancements include a 2024 combinatorial maximum flow algorithm using weighted push-relabel on shortcut graphs, achieving O(V^{2+o(1)}) time.[23] Despite these strengths, push–relabel exhibits high constant factors on sparse graphs, where its overhead in label maintenance leads to slower practical performance relative to blocking-flow methods like Dinic's.[24] To address this, hybrid approaches combine push–relabel for dense phases with Dinic's for sparse structures, improving versatility across graph densities.[25]Sample Implementations
Pseudocode for generic version
The generic push–relabel algorithm operates on a flow network with vertices, edges, source , sink , and nonnegative capacities for each edge . It maintains a preflow , which satisfies capacity constraints but may have excess flow at non-source/sink vertices, and a height labeling that estimates distances to the sink in the residual graph. Data structures include adjacency lists for residual edges, arrays for flows , residual capacities , excesses , and heights ; a set or queue of active nodes (those with , ) is used for node selection, here assuming arbitrary selection (e.g., via a list). The algorithm initializes a preflow by saturating all edges from the source, then repeatedly discharges excess from active nodes until none remain; the maximum flow value is the net flow out of the source, computed as the initial excess pushed from minus any final excess at (typically zero).[3] The following annotated pseudocode presents the core procedures: initialization, push (sends excess along an admissible residual edge where and ), relabel (increases height when no admissible edge exists), discharge (repeatedly pushes or relabels until excess is zero or height bound reached), and the main loop (selects active nodes arbitrarily until the preflow is a valid maximum flow). This version uses a simple current-arc representation for efficiency in scanning neighbors, resetting to the first neighbor after relabel.[3][9]function Initialize-Preflow(G, s, t, c):
for each vertex u in V:
h[u] ← 0 // Initialize heights to 0, except source
e[u] ← 0 // Initialize excesses to 0
h[s] ← n // Source height is number of vertices
for each edge (s, v) in E: // Saturate outgoing edges from source
f[s][v] ← c[s][v]
f[v][s] ← -c[s][v] // Antisymmetric flow
e[v] ← e[v] + c[s][v] // Add excess to neighbors
cf[s][v] ← 0 // Residual capacity forward is 0
cf[v][s] ← c[s][v] // Residual backward is full
for other edges (u, v):
f[u][v] ← 0
cf[u][v] ← c[u][v]
cf[v][u] ← 0
Active ← {u ∈ V \ {s, t} | e[u] > 0} // Set of active nodes
for each u in V:
current[u] ← first neighbor of u // Initialize current-arc pointers
function Push(u, v):
δ ← min(e[u], cf[u][v]) // Amount to push: min of excess and residual capacity
f[u][v] ← f[u][v] + δ
f[v][u] ← f[v][u] - δ
e[u] ← e[u] - δ // Reduce sender's excess
e[v] ← e[v] + δ // Increase receiver's excess
cf[u][v] ← cf[u][v] - δ
cf[v][u] ← cf[v][u] + δ
if cf[u][v] == 0: // If edge saturated, advance current arc
current[u] ← next neighbor after v
if e[v] > 0 and v ≠ s and v ≠ t:
add v to Active // Activate receiver if it gains excess (non-source/[sink](/page/Sink))
function Relabel(u):
min_height ← ∞
for each neighbor v of u: // Scan all residual edges from u
if cf[u][v] > 0:
min_height ← min(min_height, h[v])
if min_height < ∞:
h[u] ← min_height + 1 // Set new height to one more than minimum neighbor height
else:
h[u] ← 2 n // Bound height when no path to sink
current[u] ← first neighbor of u // Reset current arc after relabel
function Discharge(u):
while e[u] > 0 and h[u] < 2 n: // Continue until excess is pushed or height bound reached
v ← current[u] // Get current neighbor
if v is a valid neighbor and cf[u][v] > 0 and h[u] == h[v] + 1: // Admissible edge
Push(u, v)
else: // No admissible edge from current; try next or relabel
if v == last neighbor of u: // Scanned all, no admissible
Relabel(u)
else:
current[u] ← next neighbor after v // Advance to next
function PushRelabel(G, s, t, c):
Initialize-Preflow(G, s, t, c)
while Active is not empty: // Main loop: process active nodes arbitrarily
select u arbitrarily from Active // Generic selection (e.g., pop from list)
Discharge(u)
if e[u] > 0: // Still active after discharge (height increased)
keep u in Active
else:
remove u from Active
max_flow ← ∑_{u} f[u][t] // net flow into sink, or equivalently initial ∑ c[s][v] - e[s] (usually 0)
return max_flow
function Initialize-Preflow(G, s, t, c):
for each vertex u in V:
h[u] ← 0 // Initialize heights to 0, except source
e[u] ← 0 // Initialize excesses to 0
h[s] ← n // Source height is number of vertices
for each edge (s, v) in E: // Saturate outgoing edges from source
f[s][v] ← c[s][v]
f[v][s] ← -c[s][v] // Antisymmetric flow
e[v] ← e[v] + c[s][v] // Add excess to neighbors
cf[s][v] ← 0 // Residual capacity forward is 0
cf[v][s] ← c[s][v] // Residual backward is full
for other edges (u, v):
f[u][v] ← 0
cf[u][v] ← c[u][v]
cf[v][u] ← 0
Active ← {u ∈ V \ {s, t} | e[u] > 0} // Set of active nodes
for each u in V:
current[u] ← first neighbor of u // Initialize current-arc pointers
function Push(u, v):
δ ← min(e[u], cf[u][v]) // Amount to push: min of excess and residual capacity
f[u][v] ← f[u][v] + δ
f[v][u] ← f[v][u] - δ
e[u] ← e[u] - δ // Reduce sender's excess
e[v] ← e[v] + δ // Increase receiver's excess
cf[u][v] ← cf[u][v] - δ
cf[v][u] ← cf[v][u] + δ
if cf[u][v] == 0: // If edge saturated, advance current arc
current[u] ← next neighbor after v
if e[v] > 0 and v ≠ s and v ≠ t:
add v to Active // Activate receiver if it gains excess (non-source/[sink](/page/Sink))
function Relabel(u):
min_height ← ∞
for each neighbor v of u: // Scan all residual edges from u
if cf[u][v] > 0:
min_height ← min(min_height, h[v])
if min_height < ∞:
h[u] ← min_height + 1 // Set new height to one more than minimum neighbor height
else:
h[u] ← 2 n // Bound height when no path to sink
current[u] ← first neighbor of u // Reset current arc after relabel
function Discharge(u):
while e[u] > 0 and h[u] < 2 n: // Continue until excess is pushed or height bound reached
v ← current[u] // Get current neighbor
if v is a valid neighbor and cf[u][v] > 0 and h[u] == h[v] + 1: // Admissible edge
Push(u, v)
else: // No admissible edge from current; try next or relabel
if v == last neighbor of u: // Scanned all, no admissible
Relabel(u)
else:
current[u] ← next neighbor after v // Advance to next
function PushRelabel(G, s, t, c):
Initialize-Preflow(G, s, t, c)
while Active is not empty: // Main loop: process active nodes arbitrarily
select u arbitrarily from Active // Generic selection (e.g., pop from list)
Discharge(u)
if e[u] > 0: // Still active after discharge (height increased)
keep u in Active
else:
remove u from Active
max_flow ← ∑_{u} f[u][t] // net flow into sink, or equivalently initial ∑ c[s][v] - e[s] (usually 0)
return max_flow
Code example in Python
The following Python implementation realizes an optimized variant of the push–relabel algorithm using FIFO node selection via a deque for active nodes and the current-arc technique to maintain a pointer for each node's edge traversal, avoiding redundant scans of non-admissible edges. This approach enhances efficiency over naive edge rescanning, as detailed in the original formulation. The graph is represented with 2D lists for residual capacities and original capacities, suitable for small to medium-sized networks (n ≤ 1000), and adjacency lists for ordered neighbor access. The implementation relies solely on Python's standard library, specificallycollections.deque for the FIFO queue.
import collections
class PushRelabel:
def __init__(self, n, source, sink, capacity):
"""
Initialize the push-relabel solver.
:param n: Number of nodes (0 to n-1).
:param source: Source node index.
:param sink: Sink node index.
:param capacity: n x n capacity matrix (list of lists).
"""
self.n = n
self.source = source
self.[sink](/page/Sink) = sink
self.residual = [[0] * n for _ in range(n)]
self.original_cap = [[0] * n for _ in range(n)]
for u in range(n):
for v in range(n):
self.residual[u][v] = capacity[u][v]
self.original_cap[u][v] = capacity[u][v]
self.adj = [[] for _ in range(n)]
for u in range(n):
for v in range(n):
if self.residual[u][v] > 0 or self.residual[v][u] > 0:
if v not in self.adj[u]:
self.adj[u].append(v)
self.excess = [0] * n
self.height = [0] * n
self.height[source] = n
self.current_edge = [0] * n
self.active = collections.deque()
self.seen = [False] * n
def add_excess(self, u):
if u != self.sink and self.excess[u] > 0 and not self.seen[u]:
self.active.append(u)
self.seen[u] = True
def push(self, u, v):
push_amt = min(self.excess[u], self.residual[u][v])
self.excess[u] -= push_amt
self.excess[v] += push_amt
self.residual[u][v] -= push_amt
self.residual[v][u] += push_amt
if self.excess[v] > 0:
self.add_excess(v)
return push_amt > 0
def relabel(self, u):
min_h = float('inf')
for v in self.adj[u]:
if self.residual[u][v] > 0:
min_h = min(min_h, self.height[v])
if min_h < float('inf'):
self.height[u] = min_h + 1
else:
self.height[u] = self.n # Bound height when no outgoing residual edges
def discharge(self, u):
i = self.current_edge[u]
while self.excess[u] > 0 and self.height[u] < self.n:
if i < len(self.adj[u]):
v = self.adj[u][i]
if self.residual[u][v] > 0 and self.height[u] == self.height[v] + 1:
self.push(u, v)
i += 1
else:
i += 1
self.current_edge[u] = i
else:
self.relabel(u)
self.current_edge[u] = 0
i = 0
def max_flow(self):
# Initialize preflow from source
for v in range(self.n):
if self.residual[self.source][v] > 0:
amt = self.residual[self.source][v]
self.residual[self.source][v] = 0
self.residual[v][self.source] += amt
self.excess[v] += amt
self.add_excess(v)
# Main push-relabel loop with FIFO selection
while self.active:
u = self.active.popleft()
self.seen[u] = False
self.discharge(u)
if self.excess[u] > 0 and self.height[u] < self.n:
self.active.append(u)
self.seen[u] = True
return self.excess[self.sink]
def get_flow(self):
flow = {}
for u in range(self.n):
for v in range(self.n):
if self.original_cap[u][v] > 0:
f = self.original_cap[u][v] - self.residual[u][v]
if f > 0:
flow[(u, v)] = f
return flow
import collections
class PushRelabel:
def __init__(self, n, source, sink, capacity):
"""
Initialize the push-relabel solver.
:param n: Number of nodes (0 to n-1).
:param source: Source node index.
:param sink: Sink node index.
:param capacity: n x n capacity matrix (list of lists).
"""
self.n = n
self.source = source
self.[sink](/page/Sink) = sink
self.residual = [[0] * n for _ in range(n)]
self.original_cap = [[0] * n for _ in range(n)]
for u in range(n):
for v in range(n):
self.residual[u][v] = capacity[u][v]
self.original_cap[u][v] = capacity[u][v]
self.adj = [[] for _ in range(n)]
for u in range(n):
for v in range(n):
if self.residual[u][v] > 0 or self.residual[v][u] > 0:
if v not in self.adj[u]:
self.adj[u].append(v)
self.excess = [0] * n
self.height = [0] * n
self.height[source] = n
self.current_edge = [0] * n
self.active = collections.deque()
self.seen = [False] * n
def add_excess(self, u):
if u != self.sink and self.excess[u] > 0 and not self.seen[u]:
self.active.append(u)
self.seen[u] = True
def push(self, u, v):
push_amt = min(self.excess[u], self.residual[u][v])
self.excess[u] -= push_amt
self.excess[v] += push_amt
self.residual[u][v] -= push_amt
self.residual[v][u] += push_amt
if self.excess[v] > 0:
self.add_excess(v)
return push_amt > 0
def relabel(self, u):
min_h = float('inf')
for v in self.adj[u]:
if self.residual[u][v] > 0:
min_h = min(min_h, self.height[v])
if min_h < float('inf'):
self.height[u] = min_h + 1
else:
self.height[u] = self.n # Bound height when no outgoing residual edges
def discharge(self, u):
i = self.current_edge[u]
while self.excess[u] > 0 and self.height[u] < self.n:
if i < len(self.adj[u]):
v = self.adj[u][i]
if self.residual[u][v] > 0 and self.height[u] == self.height[v] + 1:
self.push(u, v)
i += 1
else:
i += 1
self.current_edge[u] = i
else:
self.relabel(u)
self.current_edge[u] = 0
i = 0
def max_flow(self):
# Initialize preflow from source
for v in range(self.n):
if self.residual[self.source][v] > 0:
amt = self.residual[self.source][v]
self.residual[self.source][v] = 0
self.residual[v][self.source] += amt
self.excess[v] += amt
self.add_excess(v)
# Main push-relabel loop with FIFO selection
while self.active:
u = self.active.popleft()
self.seen[u] = False
self.discharge(u)
if self.excess[u] > 0 and self.height[u] < self.n:
self.active.append(u)
self.seen[u] = True
return self.excess[self.sink]
def get_flow(self):
flow = {}
for u in range(self.n):
for v in range(self.n):
if self.original_cap[u][v] > 0:
f = self.original_cap[u][v] - self.residual[u][v]
if f > 0:
flow[(u, v)] = f
return flow
capacity = [
[0, 3, 3, 0], # Node 0
[0, 0, 2, 2], # Node 1
[0, 0, 0, 3], # Node 2
[0, 0, 0, 0] # Node 3
]
capacity = [
[0, 3, 3, 0], # Node 0
[0, 0, 2, 2], # Node 1
[0, 0, 0, 3], # Node 2
[0, 0, 0, 0] # Node 3
]
pr = PushRelabel(4, 0, 3, capacity)
flow_value = pr.max_flow()
print(f"Maximum flow: {flow_value}")
flows = pr.get_flow()
print("Flows:", flows)
pr = PushRelabel(4, 0, 3, capacity)
flow_value = pr.max_flow()
print(f"Maximum flow: {flow_value}")
flows = pr.get_flow()
print("Flows:", flows)

