10
Partial Implementation of Max Flow and Min Cost Flow in Almost-Linear Time
Abstract
In 2022, Chen et al. proposed an algorithm in [2] that solves the min cost flow problem in time, where is the number of edges in the graph, is an upper bound on capacities and is an upper bound on costs. However, as far as the authors of [2] know, no one has implemented their algorithm to date. In this paper, we discuss implementations of several key portions of the algorithm given in [2], including the justifications for specific implementation choices. For the portions of the algorithm that we do not implement, we provide stubs. We then go through the entire algorithm and calculate the term more precisely. Finally, we conclude with potential directions for future work in this area.
Acknowledgments
I would like to thank Dr. Adam Hesterberg for offering guidance throughout this project as my senior thesis advisor during the 2023-2024 academic year. He was extremely helpful in discussing ideas and offering feedback on this paper, and I could not have done this project without him. I would also like to thank Professors Yang Liu, Sushant Sachdeva and Richard Peng for answering clarifying questions about the algorithm that they present in [2] and offering advice on how I go about implementing things in this project. Their insight was extremely helpful. I am also thankful for Professors Daniel Sleator and Robert Tarjan for their insight on Dynamic Trees. I also sincerely appreciate Professor Madhu Sudan for agreeing to read and evaluate my thesis.
In addition, I want to thank Mincheol, Kevin, Ryan, Skyler, Sunera, Wittmann, and Yiting for being my suitemates and being part of an unforgettable experience at Harvard. I’ve really enjoyed these past 4 years with them. I am also thankful to my parents and older sister for all of their support throughout my Harvard education.
Lastly, I thank the Harvard Computer Science Department and Harvard College for giving me the opportunity to write a senior thesis and undertake this project.
1 Introduction
The maximum-flow problem is formulated as follows:
Suppose we have a connected graph with edges and vertices where one vertex is designed as a source and another is designated as a sink . Each edge in has a given capacity We want to the maximize the amount of flow from to such that for all vertices the flow out of is equal to the flow into and that for any edge , the flow along edge satisfies
To solve this problem, Ford and Fulkerson created the Ford-Fulkerson algorithm, which runs in time where is the max flow. There is also the Edmonds-Karp algorithm which runs in time which does not depend on the size of the max flow itself.
There is also the related minimum-cost flow problem, where each edge is also assigned a cost along with its capacity. Then the goal is to push the maximum total flow from the source to the sink while minimizing the total cost along the way.
Lastly, we can combine these two ideas as follows: Letting the max flow as defined above be the min cost flow finds the minimum possible total cost of the flow over the edges such that all requirements of the max flow problem are met and the max flow is attained.
In 2022, Chen, Kyng, Liu, Peng, Gutenberg and Sachdeva wrote [2] in which they found an algorithm to compute both maximum flows and minimum-cost flows in time, which is the asymptotically fastest known algorithm to date. In this paper, we work on implementing the algorithm given in [2]. As far as the authors of [2] are aware, this algorithm has never been implemented before, and this paper constitutes the first attempt at implementing it. This algorithm relies on a number of subparts, including dynamic trees, low-stretch spanning trees, and undirected minimum-ratio cycles. Apart from very well-known things (like binary trees), there do not appear to be existing implementations of any of these subparts that meet the asymptotic time complexities given in [2].
We implement some of these subparts in the following sections in Python. Although Python is slower than many other languages by a large constant factor, the algorithm in [2] was never designed to be fast in practice, so this is not an issue. At the beginning of each section, we briefly describe what code that section corresponds to and explain how that code fits into the broader algorithm. In addition, for the sections where we do not implement all of the code described in the corresponding subpart, we include a description of what we did implement and what was challenging about the parts we did not implement. All of the code referenced in this paper can be found at the following Github: https://github.com/thinkinavi24/almost_linear_max_flow. The generative AI tool ChatGPT was used to write a significant amount of this code and is credited accordingly in comments of the Jupyter codeblocks. In multiple Jupyter notebooks, some codeblocks were written by ChatGPT while others were written by hand; any code block not labeled as being written by ChatGPT was written by hand. All code written by generative AI was thoroughly checked, tested and revised as needed as was all other code in the Github.
2 Preliminaries
We begin with some definitions that we will refer back to throughout this paper. Definitions, notations and remarks with references in this section are copied identically from those references and are reproduced here for convenience. In addition, in later sections throughout this paper, we will quote results including theorems, definitions and lemmas from other articles and we will mention the source of the result immediately before reproducing it. Further, when discussing the implementation of a list of methods/attributes in a data structure that are described in another paper, we will often quote that paper’s description of the methods/attributes. When we do so, we will mention the source of the result immediately before reproducing a list that is copied from the referenced paper.
2.1 Definitions
Definition 2.1.
Given a graph for all pairs of vertices we define to be the shortest path distance from to on
Definition 2.2.
([1]) The radius of a connected graph around a point is where represents the shortest distance from to for each vertex
Notation 2.3.
([2]) We let a graph correspond to its vertices and its edges , and we let and Sometimes, we may explicitly write to denote the edges of or to denote the vertices of Further, we let denote an edge of and denote a vertex of We assume that each edge has an implicit direction, used to define its edge-vertex incidence matrix
Definition 2.4.
([2]) We let denote the degree of the vertex in or the number of incident edges. We let and denote the maximum and minimum degree of graph
Notation 2.5.
For a graph with vertices for any subset we let denote
Definition 2.6.
([2]) We define the volume of a set as
Definition 2.7.
([2]) Given graphs and with we say that is a graph-embedding from into if it maps each edge to a path in .
Definition 2.8.
([2]) The congestion of an edge is defined as and of the embedding by
Definition 2.9.
([2]) The congestion of a vertex is defined by Further, the vertex-congestion of the embedding is
Definition 2.10.
([2]) The length of an embedding is defined as
Definition 2.11.
([2]) is a dynamic graph if it undergoes batches of updates consisting of edge insertions/deletions and/or vertex splits that are applied to We say that the graph , after applying the first update batches is at stage and denote the graph at this stage by
Remark 2.12.
([2]) For each update batch we encode edge insertions by a tuple of tail and head of the new edge and deletions by a pointer to the edge that is about to be deleted. We further also encode vertex splits by a sequence of edge insertions and deletions as follows: if a vertex is about to be split and the vertex that is split off is denoted , we can delete all edges that are incident to but should be incident to from and then re-insert each such edge via an insertion (we allow insertions to new vertices that do not yet exist in the graph). For technical reasons, we assume that in an update batch , the updates to implement the vertex splits are last, and that we always encode a vertex split of into and such that We let the vertex set of graph consist of the union of all endpoints of edges in the graph (in particular if a vertex is split, the new vertex is added due to having edge insertions incident to this new vertex in
Definition 2.13.
We say a function if there exist constants such that for sufficiently large Note that this is equivalent to standard big O-notation except we also allow for arbitrary powers of .
Definition 2.14.
([2]) We define ENC() of an update to be the size of the encoding of the update and note that for edge insertions/deletions, we have ENC() = and for a vertex split of into and as described above we have that ENC() = For a batch of updates , we let ENC() = ENC().
Remark 2.15.
([2]) The number of updates in an update batch can be completely different from the actual encoding size ENC of the update batch .
Definition 2.16.
([5]) For any subsets we denote as the set of edges between and
Definition 2.17.
([5]) The cut-size of a cut is
Definition 2.18.
([5]) The conductance of a cut in is Unless otherwise noted, when speaking of the conductance of a cut we assume to be the side of smaller volume. The conductance of a graph is If is a singleton, we define
Definition 2.19.
([5]) We say a graph is a expander if , and we call a partition of a expander decomposition if
Definition 2.20.
([5]) Given and a set of vertices we say is a nearly expander in if
Definition 2.21.
([5]) Given a graph , its subdivision graph is the graph where we put a split [vertex] on each edge (including self loops). Formally, where and We will call [vertices] in the split [vertices], and the other [vertices] in the regular [vertices].
Convention 2.22.
Whenever we use in this paper, we always mean the natural logarithm. Of course, this is irrelevant when considering only asymptotic runtimes.
2.2 Layout of Algorithm
The authors in [2] present their main algorithm on page 69 at the beginning of Section 9 in Algorithm 7. In order to figure out exactly what needs to be implemented (vs portions of [2] that are not directly part of the final algorithm) we work backwards from Algorithm 7.
We begin by listing the things we need to implement for Algorithm 7. Then for each of these, we list the things we need to implement for them, and so on until we reach algorithms that can be implemented without referring to any other major algorithms. Note that some things are repeated within the list; this is because certain data structures/algorithms are used at multiple points within the overall main algorithm. The bolded items are ones which we have implemented and are labeled as fully implemented or partially implemented.
A quick glance at Algorithm 7 tells us that we need to implement the following:
In order to implement Algorithm 7, we can view this list itself as a flowchart/DAG, where the leaves of the tree need to be implemented to eventually implement our way up to the root. In order to implement the algorithm at each “node” of the tree, all of its children must be implemented.
3 Dynamic Trees
Dynamic Trees are described in Lemma 3.3 of [2] which is reproduced below for convenience:
Lemma 3.1.
([2]) There is a deterministic data structure that maintains a dynamic tree under insertion/deletion of edges with gradients and lengths and supports the following operations:
-
1.
Insert/delete edges to , under the condition that is always a tree, or update the gradient or lengths The amortized time is per change.
-
2.
For a path vector, for some , return or in time
-
3.
Maintain a flow under operations for and path vector or query the value in amortized time
-
4.
Maintain a positive flow under operations for and path vector , or query the value in amortized time .
-
5.
DETECT(). For a fixed parameter , and under positive flow updates (item 4), where is the update vector at time , returns where is the last time before that was returned by DETECT(). Runs in time
The data structure in Lemma 3.3 of [2] above is a slight extension on the Dynamic Trees described in [6]. In general, the purpose of dynamic trees is to make operations like cutting a tree or linking two trees possible in time, which can then be efficiently combined to implement operations 1-5 above also in time.
This is an important data structure for the algorithm presented in [2]. It is challenging to find an implementation of this data structure online. The authors of [6] pointed us to the following implementation in C++: https://codeforces.com/contest/487/submission/8902202, written by the first author of [6]. They also pointed us to their paper [7] which provides a simpler description for Dynamic Trees.
Since an implementation of Dynamic Trees already exists, though it is in a different language, we do not implement one here and instead defer to that implementation. While that implementation does not include a DETECT() method, the authors in [2] give an algorithm for DETECT() which should be implementable given the Dynamic Tree Class linked above. Even though we are not creating our own dynamic trees implementation, we will refer to Lemma 3.1 throughout this paper so it is important to understand their definition as given by Lemma 3.1.
4 Low Stretch Decomposition
As described in Section 2.2, in this section we implement Low Stretch Decomposition as specified in Lemma 6.5 of [2], which states the following:
Lemma 4.1.
(Dynamic Low Stretch Decomposition). There is a deterministic algorithm with total runtime that on a graph with lengths weights and parameter , initializes a tree spanning and a rooted spanning forest a edge-disjoint partition of into sub trees and stretch overestimates The algorithm maintains decrementally against batches of updates to say such that for any new edge added by either edge insertions or vertex splits, and:
-
1.
has initially connected components and more after update batches of total encoding size satisfying
-
2.
for all at all times, including inserted edges
-
3.
-
4.
Initially, contains subtrees. For any piece and at all times, where is the set of roots in Here, denotes the set of boundary vertices that are in multiple partition pieces.
The factor will be specified in Notation 4.3.
We begin by implementing low-stretch spanning trees in Section 4.1 before moving onto the Heavy Light Decomposition in Section 4.2.
4.1 Low-stretch spanning trees
We begin by writing code to implement the LSST algorithm described in [1]. The code can be found in the file titled petal.ipynb.
The main result of [1] can be summarized as follows:
Theorem 4.2.
Suppose is a connected, undirected graph with vertices and edges where and . Then we can construct a spanning tree such that Further, this tree can be found in time.
The tree found in Theorem 4.2 is our low-stretch spanning tree (LSST).
Notation 4.3.
Following the notation of [2], we will be referring to the stretch factor of the LSST as for the remainder of the paper.
The algorithm can be understood through a few functions: create_petal, petal_decomposition and hierarchical_petal_decomposition. We describe the implementation of each of these functions below. In order to get our LSST, we call hierarchical_petal_decomposition on our original graph with specific parameters that are specified in Section 3.3.
4.1.1 create_petal
Abraham and Neiman give two algorithms in [1] for create-petal which both take in a graph, a starting point a target and a radius (where is expressed as a range in the first algorithm) and which both return a cluster (which is the petal) as well as a center of that petal. The algorithm given on page 235 is not fast enough for the overall desired bound in this paper: it requires that the ratio between the largest and smallest distances is for some which does not necessarily hold in general, and the create-petal algorithm has a for loop which could potentially run for many iterations, and there is no clear bound on how many iterations this would take. For this reason, we implement the faster algorithm on page 245 in section 6 of [1], which the authors themselves state is necessary for the best asymptotic efficiency.
In the code, we begin with an ordinary implementation of Dijkstra’s Algorithm using Fibonacci Heaps along with a corresponding shortest_path function. Everything in this portion is standard. After that, we write a function to generate the directed graph as specified on page 245 of [1]. As Abraham and Neiman point out, all edges on the directed graph have nonnegative length.
With that out of the way, we can write the create_petal function. In essence, this algorithm iteratively carves petals off of the graph and then carves petals off of the remaining part of the graph until all petals have been carved, when any remaining vertices are made part of the stigma of the flower. This function takes in the following inputs: a graph , a target , a starting point and a radius Note that this is slightly different from the slower function that Abraham and Neiman propose. In particular, the create_petal function does not take in the original graph as input except at the beginning; rather, as described in the following subsection, each time a petal is carved, create_petal is iteratively called on the remaining subgraph. We also do not have any hi and lo parameters, as the faster algorithm described on page 245 of [1] does not include any.
To create a petal, we first run Dijkstra on the given graph with the given starting point We then generate the corresponding directed graph based on the shortest distances from to all other points in the graph and calculate the shortest path to the target Next, we need to find the special vertex , defined on page 245 of [1] as the furthest point from on the shortest path from to in the original graph that is within distance of To do this, we could perform binary search on the sorted distances on the shortest path from to in order to find the largest number less than or equal to However, in our implementation we simply do a linear search as this does not impact the overall asymptotic time complexity, which we empirically check later.
Next, the algorithm stipulates that the petal is the ball of radius around in the directed graph where each edge on the path from to is set to half of its original weight in At first, this may seem to be equivalent to the following: include every vertex within the ball of radius and then also include every vertex on the path from to including This would make intuitive sense because is defined as the furthest point along the path from to within distance of . If all of the closest points to are further than away, then An implementation which does this would not be quite right for the following reason: while edges in the directed graph all have nonnegative length by the triangle inequality, they can have length 0, and this fact influences the implementation of create_petal. Therefore, in the directed graph, may not be the furthest point within distance Edges of length 0 mean that some points “further” along the path may be of equal distance.
Therefore, it is essential to first set the weight of each edge on the path from to to half its original weight in After that, we run Dijkstra on the directed graph and select all vertices within distance of
The function then returns the petal and the point which is the center of the petal and plays a key role in the petal decomposition. In addition, we also choose to return the shortest path from to , since this will come up in the petal_decomposition function described in the next subsection.
4.1.2 petal_decomposition
In our implementation of this function, we begin with some sanity checks that the center and target are both in the graph. While this may seem unnecessary, we will see in the next subsection that there are many recursive calls which involve modifying the graph. These assert and raise statements helped catch many errors earlier on in the implementation and ensured everything worked properly once those errors were corrected.
We begin by computing the radius of graph, defined as the maximum length across all vertices of the shortest path from to found using Dijkstra. We then initialize the following lists with their defined purposes:
-
•
Ys: is defined as the original graph minus the vertices and edges contained in petals . In particular, is initialized to the entire graph before any petals have been removed.
-
•
Xs: is the petal found on iteration and is carved from , except for which is the remaining part of the graph after all petals have been carved (what the authors in [1] call the “stigma” of the flower)
-
•
xs: is the center of petal or the point when computing the petal , except for which is the original input
-
•
ts: is the target when creating petal
-
•
ys: is the neigbhor of on the path from to which is closer to
The rest of the implementation follows the pseudocode. The first petal that is carved may be a special petal if the distance from to the target is greater than of the radius from Abraham and Neiman describe why they choose the constant in [1]. The purpose of this petal is to preserve the shortest path from to
To carve all of the other petals, we iteratively check for points whose distance from is at least If no other points exist, we have found all petals. Otherwise, we arbitrarily choose one of those points to be the target for petal In our implementation, we simply choose the first key in the dictionary of key value pairs where the keys are vertices and the values are distances from the center , but this is arbitrary because the keys are only ordered as they were in the original input graph, which is arbitrary.
We then call create_petal on the remaining part of the original graph (denoted by the element most recently appended to the Ys list) along with our center and target For the radius, we use . This choice comes from the pseudocode for petal_decomposition in [1], where the call to create_petal sets lo = 0 and hi = However, as we recall from the previous subsection, the faster implementation of create_petal does not have any lo or hi parameters but rather just a single radius parameter. For this reason, it was not obvious whether to use or something like However, we decided to use because this parameter is a radius and all points within this radius will have distance less than or equal to from the center. For this reason, having a max possible value of (the hi parameter) vs a single value of seemed more analogous. However, that is not a fully rigorous argument, and the testing described in Section 4.1.4 is what provides empirical validation for this choice.
Once we create the petal, we subtract it off from the remaining part of the graph. Using the path from to , we compute , which will be used in the following subsection. We then halve all of the edges on the shortest path to (but beginning at rather than ) returned by the create_petal function as given in the pseudocode.
Finally, we let be the last , namely the remaining part of the graph after all petals have been carved.
4.1.3 hierarchical-petal-decomposition
This is the third and final function necessary to generate a low-stretch spanning tree. For inputs, it takes in the graph, a starting point and a target This function is recursive with a base case that the tree on a graph with one vertex is just the single vertex itself.
On each recursive call, we compute the petal decomposition of the graph. As explained in the previous subsection, this returns a set of petals , their centers , a set of neighbors and a set of targets . We then run hierarchical-petal-decomposition recursively on each petal where the starting point is and the target is Each of these recursive calls generates a tree for each petal These trees are all disjoint, so we create a new graph which combines all of these through the edges This gives us a final low-stretch spanning tree
Lastly, to compute the tree itself, we call hierarchical-petal-decomposition on the graph with arbitrary starting point but critically also with target The justification for this decision is explained in [1].
4.1.4 LSST Implementation Validation
The function hierarchical_petal_decomposition was the last function described in [1]. In order to test the LSST implementation, we generate many Erdős-Rényi random graphs. For each random graph, we pick a probability and let there be an edge between every pair of distinct vertices with probability (so would correspond to a complete graph).
Then, for each graph, we compute an LSST using hierarchical_petal_decomposition, measuring its stretch and the duration to compute it. We repeat this for 10 trials and take the average for both the stretch and the duration. There are two primary things to verify: that the stretch is and that the time complexity is We also separately checked that the output was always a spanning tree (it reaches every vertex of the original graph and has edges). However, when timing the code, we did not include this extra check since it is not a necessary part of the algorithm once we are confident that the algorithm is running correctly.
To check the asymptotic nature of the stretch and duration to compute the LSST, for different values of we graph the stretch and duration vs We expect that there are some finite constants such that the stretch with edges and vertices is bounded above by and likewise the duration is bounded above by We check this graphically below.


In both cases, we can see that for sufficiently large values of and both the duration and stretch are bounded above by a linear constant multiplied by Of course, this does not definitively verify that the implementation is correct (for example, there could hypothetically be a factor that would not show up graphically in the experiments run above) but given that the authors in [1] prove their algorithm and stretch are both and the implementation followed their algorithm, it is reasonable to conclude that the graphs above provide empirical validation that our implementation works correctly.
4.2 Heavy Light Decomposition
In this subsection we discuss the code that implements the Heavy Light Decomposition of a tree. First, we reproduce a necessary definition from [2], which we have modified slightly to refer to general trees:
Definition 4.4.
([2]) Given a rooted tree and vertex define as the set of its ancestors in plus itself. We extend the notation to any subset of vertices by defining
Now we define the Heavy Light Decomposition as described in Lemma B.8 of [2] (which quotes [6]) which we reproduce below for convenience:
Lemma 4.5.
There is a linear-time algorithm that given a rooted tree with vertices outputs a collection of vertex disjoint tree paths (called heavy chains), such that the following hold for every vertex :
-
1.
There is exactly one heavy chain containing
-
2.
If is the heavy chain containing , at most one child of is in
-
3.
There are at most heavy chains that intersect with
In addition, edges that are not covered by any heavy chain are called light edges.
4.2.1 Implementation Details
The heavy_light_decomposition function takes in one input: the tree. Within this function, we define two inner functions: dfs_size and decompose_chain, which we describe below.
For the dfs_size function which takes in a vertex and its parent as inputs. Then, the function uses Depth First Search to recursively visit every vertex and compute the size of the subtree (number of vertices) rooted at that vertex. If the vertex is a leaf node, we say its subtree has size 1 (just itself). Otherwise, we initialize the size of the subtree to 1 and compute where is the set of all children of .
Next, we have the decompose_chain function. This function takes in a vertex , the head of the chain of and the parent of as inputs. In this function, we first identify the heavy child for that vertex, which corresponds to condition 2 in Definition 4.5 and is the one child of within , the heavy chain containing . We find this heavy child by iterating over the children of and picking the child of which has the maximum subtree size.
The decompose_chain function then recursively calls itself as long as we keep finding new “heavy children.” For each recursive call, we take in the previous heavy_child as our new vertex the same chain_head as before and the vertex as the parent (since the heavy_child is a child of vertex ).
Once the recursive calls are complete, we iterate over the children of , and for every child that isn’t the heavy child, we initialize a new chain and recursively call decompose_chain where our new vertex is the child we’re iterating over, the chain_head is the index of the current chain and the parent is our old By setting the code up in this way, we ensure that we create a chain for every vertex and its descendants and that we satisfy the requirements of Lemma 4.5.
With these two functions implemented, we can finish our heavy_light_decomposition implementation. We initialize variables for the length of the tree, the subtree sizes, and the chains. We then call dfs_size on the root vertex and its “parent” (which is not a real vertex) but which we denote This allows us to figure out the sizes of all of the subtrees in the tree. Then, we call decompose_chain on our root vertex , the chain_head which we initialize to and the parent . Finally, the heavy_light decomposition function returns the list of chains.
4.2.2 Heavy-Light Decomposition Implementation Validation
In order to check our implementation, we check the three conditions of Lemma 4.5 empirically. We produce a function that generates random trees. These trees are implemented as a list of lists, with nodes numbered to where is the root. Further, the list at index consists of the children of node , and the list is empty if and only if node is a leaf node. Based on this format, such random trees can be generated as follows: for vertex , randomly choose one of the vertices to be its parent by appending vertex to the list at index . This also has the convenient property that the parent’s number is always strictly less than the child’s number.
We then call our heavy light decomposition function on 1000 randomly generated trees. We iterate over all possible vertices for that tree and check that vertex is represented in one and only one heavy chain. We can then easily check the children of and ensure that at most one child of is in that same heavy chain.
For the third condition, we calculate for each and count the number of heavy chains that intersect with it. This is the trickiest condition to check because Lemma 4.5 only promises an bound on the number of intersecting heavy chains. To check this, we consider increasing values of and graph the average number of intersecting heavy chains vs , where the average is calculated by summing up the number of intersections across all vertices and then dividing by both the number of vertices and the number of randomly generated trees, in this case 1000. Below, we plot the average number of intersections vs , and we see that a line with slope 1.15 dominates the growth. Thus, empirically it appears that the number of intersections is bounded by In addition, we also computed the quotient of the number of intersections divided by , which gave us the following list: [1.276, 1.196, 1.144, 1.134, 1.126, 1.103, 1.092, 1.090, 1.084, 1.081, 1.079, 1.072]. We can see that this list is monotonically decreasing, which provides further assurance that the number of intersections is

In addition, we also empirically check the time complexity for our implementation just like we did for low-stretch spanning trees. Lemma 4.4 states that the algorithm should be time, which indeed appears to be the case below.

5 Rebuilding Game
The authors in [2] create a rebuilding game in order to attain their promised near linear time bound for max flow. As they describe at the beginning of section 8, this rebuilding game is the key step to go from Theorem 7.1 to Theorem 6.2 (theorem numbers are with respect to [2]). Theorem 6.2 asserts the existence of the main overall data structure in [2] that is crucial for attaining the near asymptotic linear time bound. The code for this part is included in the file titled rebuilding.ipynb. Although for our purposes we only need the rebuilding game under very specific conditions with specific parameter values, we implement it in full generality as it is described in Section 8 of [2].
5.1 Preliminaries
“The rebuilding game has several parameters: integers parameters size and depth , update frequency , a rebuilding cost , a weight range , and a recursive size reduction parameter , and finally an integer round count ” ([2]).
They define the rebuilding game in Definition 8.1 of [2] which we reproduce here for convenience:
Definition 5.1.
([2]) The rebuilding game is played between a player and an adversary and proceeds in rounds Additionally, the steps (moves) taken by the player are indexed as . Every step is associated with a tuple Both the player and adversary know . At the beginning of the game, at round and step , we initially set for all levels At the beginning each round ,
-
1.
The adversary first chooses a positive real weight satisfying This weight is hidden from the player.
-
2.
Then, while either of the following conditions hold, or for some level at least rounds have passed since the last rebuild of level the adversary can (but does not have to) force the player to perform a fixing step. The player may also choose to perform a fixing step, regardless of whether the adversary forces it or not. In a fixing step, the player picks a level and we then set for and for We call this a fix at level and we say the levels have been rebuilt. This move costs time.
-
3.
When the player is no longer performing fixing steps, the round finishes.
There are some aspects of the original explanation in [2] that were underspecified and are clarified here. The first is that the steps indexed by refer to fixing steps, and further that each step also has a level . For this reason, the step count is incremented each time a level is fixed (thus refers to the total number of fixing steps across all levels). Note that this increment of the step count is separate from the fixing counts in the fix array, which are incremented as well until they are reset.
In order to assess whether the adversary is forcing a fixing step, we check the as specified above in Definition 5.1.
5.2 Implementation and Validation
We implement this rebuilding game through two functions. We first implement a fix function which the main algorithm will call to perform a fixing step. This function is fairly routine and simply takes in a level , an index , the step index , the round and an array called prevs. The fix function returns the newly updated prevs array after the fix is complete.
Next, we have our main rebuilding function. In addition to the parameters listed above, we also take in the parameter which represents a 2d array where the row of is and we initialize as explained in Remark 8.2 of [2]. We also initialize 1D arrays to keep track of the fix and round counts and a 2D prevs array. The prevs array has columns (one for each level) and initially one row of all ones as outlined in Definition 5.1 above.
We provide empirical validation that the algorithm runs in For setting the values of each of these variables, we refer to Section 8.2 of [2], which gives asymptotic values for the variables. We let and While section 8.2 of [2] states that as opposed to we note that scaling by a constant has no bearing on the runtime, and the authors also state many other times in [2]. The reason for the constant factor of in this implementation is because otherwise we would always have unless was extremely large, and there is no reason to always set The exact value of is arbitrary and can be replaced by any other positive integer.
In addition, we also let be a vector with entries where the are i.i.d Uniform(1, 1000). Here, the number 1000 is arbitrary. Lastly, we set in order to ensure that as required by [2].
With these parameters, we run experiments with We plot the durations for each of these values of vs As promised by [2], we do empirically observe that the runtime increases linearly with and is therefore bounded by a constant times where this constant is the slope of the red line in the graph below or This number comes from calculating the slope between the last points and one of the preceding points and increasing the y-intercept slightly, as the graph appears to be progressing linearly once the axis exceeds

6 Interior Point Method
In Section 4 of [2], the authors present an interior point method that solves the min-cost flow problem.
6.1 IPM Preliminaries
We begin by defining the key variables that will be used throughout this section and the remainder of the paper as they are defined in [2]. In the subsequent text, recall that was defined in Notation 2.3.
Definition 6.1.
For our min-cost flow problem, we let represent the demands. We also let the lower and upper capacities be denoted by where all integers are bounded by the variable . Finally, we let denote the costs.
Definition 6.2.
We define We will use to create a barrier when solving the L1 IPM.
Definition 6.3.
We define:
Definition 6.4.
We let
We now reproduce Theorem 4.3 of [2], which is their main result regarding IPMs for solving the min-cost flow problem ( is the flow after iteration , while the optimal flow is unknown):
Theorem 6.6.
([2]) Suppose we are given a min-cost flow instance given by Equation (1). Let denote an optimal solution to the instance.
For all there is a potential reduction interior point method for this problem, that, given an initial flow such that the algorithm proceeds as follows:
The algorithm runs for iterations. At each iteration, let denote that gradient and denote the lengths given by Definition 6.5. Let and be any vectors such that and
-
1.
At each iteration, the hidden circulation satisfies
-
2.
At each iteration, given any satisfying and it updates for
-
3.
At the end of iterations, we have
6.2 IPM Implementation
All code described in this section can be found in the file ipm.ipynb.
In [2], the authors give a fairly detailed implementation that relies on specific bounds on the gradient, number of iterations etc. In order to implement that version of an interior point method, we would have to implement an oracle for a specific step described in Section 4 of [2], which is nontrivial to implement.
In this section we merely provide an approximation/heuristic for solving the interior point method which relies on scipy.optimize. We define the objective function as given in Definition 6.4 and feed the constraints as well. The minimize function in scipy.optimize then numerically solves for the optima of the potential function.
We empirically check the runtime of this simple implementation and compare it to the runtime promised above by Theorem 6.6, where we let as defined later on in [2] (they say but an extra constant factor doesn’t change the following analysis).
![[Uncaptioned image]](https://cdn.awesomepapers.org/papers/28dc19e9-150a-417f-83dd-6ac3d5380025/ipm.png)
We notice that the graph is decreasing, which is surprising. Empirically checking the values of , we find they are decreasing over the values from to In order to inspect this, we take the log of this expression, where we see:
Specifically, in the final expression above, we know that will go to while the expression in the parentheses will go to . Thus in the limit the log goes to infinity, which means the original expression goes to infinity as well. Yet the empirically decreasing nature of the function in the graph above raises a question: when does this function start increasing?
To find this, we take a derivative of the logged expression. We first let since only appears as Then, we have:
We want to find out when the derivative is positive. If we take , we see that the derivative approaches but if we plug in a number like (which, we keep in mind corresponds to ) we get This function is continuous, so by the Intermediate Value Theorem, we know that it will cross at some point. We will find out where the derivative equals (which corresponds to a local optimum) and then argue that it increases after this point.
Setting our above expression equal to , we get:
If we plug this expression into Wolfram Alpha, we get which we will write as (but all numerical calculations below are done using the more precise value of ). Now, we want to confirm that the derivative is positive whenever We have:
The derivative of the left hand side is while the derivative of the right hand side is Then, we see:
From Wolfram Alpha, we see this is true whenever (which makes sense as ) and This means when is increasing faster than so the first derivative will be positive for and is a local minimum of the differentiated expression.
Returning to our original expression, we recall that so our original function is increasing when When we have . Plugging these values in, at this local minimum, we have:
As stated previously, all calculations are done using the precise values of each numerical quantity rather than the rounded values we use above.
Simplifying, this comes out to which is of course extremely, extremely close to . Thus, even though our function eventually tends to infinity, it first approaches . Further, given that is many orders of magnitude larger than the number of atoms in the known universe (approximately ), it is not possible to empirically check whether our scipy implementation or (or even a fully correct implementation) matches the asymptotic growth of
For this reason, it does not make sense practically to consider the runtime of the IPM relative to its asymptotic guarantee. For any future implementations of the IPM algorithm described by Theorem 6.6, the focus should be on the other requirements of Theorem 6.6. With that said, it’s hard to test the other requirements either. While we could calculate via another algorithm to try to test our IPM implementation for accuracy, the scipy.optimize method that we are using only returns the final converged value rather than the successive steps which makes requirements 1 and 2 of Theorem 6.6 infeasible to check. Even if we had the successive step values, it is unlikely that this heuristic would exactly match those requirements. For requirement 3, we note that the bound only holds after requires iterations which could be extremely large practically, which suggests that the scipy implementation may run into numerical issues or may not converge in time, which makes it unreliable for testing. Thus, while the scipy implementation provides a heuristic for solving the IPM problem, it is not complete.
7 Overall Time Complexity
The authors in [2] state that their max flow algorithm is time or what they sometimes call time. The goal of this section is to more precisely calculate the term.
For a graph with edges and vertices to be connected, we must have at least edges. Therefore, which implies that This allows us to bound by in the asymptotic calculations that follow.
We refer to Algorithm 7 of [2], which provides the algorithm for their main theorem. We then refer to each component of it and calculate its runtime based on what the authors in [2] say. In cases when they simply say we take a deeper dive as needed to give a more precise asymptotic time complexity.
7.1 Preliminaries
We begin by reproducing several Lemmas and Theorems from [2] which we will refer to throughout the subsequent subsections:
Definition 7.1.
([2]) Consider a dynamic graph undergoing batches of updates consisting of edge insertions/deletions and vertex splits. We say the sequences and satisfy the hidden stable-flow chasing property if there are hidden dynamic circulations and hidden dynamic upper bounds such that the following holds at all stages :
-
1.
is a circulation:
-
2.
upper bounds the length of : for all
-
3.
For any edge in the current graph , and any stage if the edge was already present in i.e. then
-
4.
Each entry of and is quasipolynomially lower and upper-bounded: and for all
Definition 7.2.
Definition 7.3.
([2]) Given a graph , forest , and parameter , define a -sparsified core graph with embedding as a subgraph and embedding satisfying
-
1.
For any , all edges satisfy
-
2.
and
-
3.
has at most edges.
-
4.
The lengths and gradients of edges in are the same in (Definition 7.2).
Definition 7.4.
([2]) For a graph parameter and branching factor a B-branching tree-chain consists of collections of graphs such that and we define inductively as follows,
-
1.
For each , we have a collection of trees and a collection of forests such that satisfy the conditions of Lemma 4.1.
-
2.
For each and we maintain -sparsified core graphs and embeddings and
-
3.
We let
Finally, for each we maintain a low-stretch tree
We let a tree chain be a single sequence of graphs such that is the -sparsified core graph with embedding for some for and a low-stretch tree on
Definition 7.5.
([2]) Given a graph and tree-chain where define the corresponding spanning tree of as the union of preimages of edges of in
Define the set of trees corresponding to a branching tree-chain of graph as the union of over all tree chains where :
Definition 7.6.
Lemma 7.7.
([2]) There is a deterministic strategy given by Algorithm 6 for the player to finish rounds of the rebuilding game in time
Theorem 7.8.
([2]) Let be a dynamic graph undergoing batches of updates containing only edge insertions/deletions with edge gradient and length such that the update sequence satisfies the hidden stable-flow chasing property (Definition 7.1) with hidden dynamic circulation and width There is an algorithm on that maintains a branching tree chain corresponding to trees (Definition 7.5), and at stage outputs a circulation represented by off-tree edges and paths on some
The output circulation satisfies and for some
where are the previous rebuild times (Definition 7.6) for the branching tree train.
The algorithm succeeds w.h.p with total runtime for Also, levels of the branching tree train can be rebuilt at any point in time.
Theorem 7.9.
([2]) There is a data structure that on a dynamic graph maintains a collection of spanning trees for , and supports the following operations:
-
•
UPDATE(): Update the gradients and lengths to and . For the update to be supported, we require that contains only edge insertions/deletions and and satisfy the hidden stable-flow chasing property (Definition 7.1) with hidden circulation and upper bounds , and for a parameter ,
-
•
QUERY(): Returns a tree for and a cycle represented as paths on (specified by their endpoints and the tree index) and explicitly given off-tree edges such that for
Over stages the algorithm succeeds w.h.p with total run time for
7.2 Rebuilding Game
Now, we analyze the time complexity of the Rebuilding Game, which we know is given as in Lemma 7.7. At the end of Section 8.2 of [2], when describing how to use the Rebuilding game to get the main data structure described in Theorem 7.9, the authors state that Plugging these values in, we get the bound given in [2].
Here, as defined in Theorem 7.9, and we need to bound in terms of . To do this, we turn to Lemma 9.4 from [2], which we reproduce below, where Algorithm 7 is the name [2] gives to their overall algorithm:
Lemma 7.11.
([2]) Consider a call to MINCOSTFLOW (Algorithm 7) and let be as in line 20 [of Algorithm 7]. Then
Thus, based on Lemma 7.11, we have Here, as defined in Theorem 7.9 and where is as defined in Definiton 6.1. Further, is as defined in the DETECT operation of Lemma 3.1. We treat and as constants that we absorb into the notation.
In order to dissect what is being included in the notation above for , we refer to Section 4 of [2], specifically Theorem 6.6 and the text immediately following it, where the authors in [2] state that a certain operation decreases the potential (see Definition 6.4) by . The potential drops from to so this takes operations, where . We also note that
Now we do some big computation. Letting denote constants, we have:
The last step follows by pulling out the term and then noticing that dominates and
From this, we get:
As a sanity check, we note that because:
Now we have the following mathematical lemma:
Lemma 7.12.
Suppose and are positive functions that both go to as such that Then .
The proof of Lemma 7.12 is rather obvious: such that Thus Then, since we clearly have
Applying this logic above, it follows that so we indeed have that
7.3 MinCostFlow Algorithm
In this subsection, we precisely calculate the time complexity for the MinCostFlow Algorithm. This, combined with the previous subsection, allows us to establish the overall time complexity in the next subsection.
In Section 9 of [2], the authors show that the MINCOSTFLOW Algorithm satisfies the hypotheses of Theorem 6.6. Thus, we conclude that the MINCOSTFLOW Algorithm runs for iterations, and following the same logic as in Section 7.2, we can rewrite this as
For the time per iteration, we note that there are trees maintained by the data structure. For each of these trees, we invoke the Dynamic Tree data structure operations, which take based on the guarantees of Lemma 3.1.
Therefore, we get a bound of . We note that Thus, we still have as our time complexity.
7.4 Comparison with Edmonds Karp
The Edmonds Karp algorithm is another well known algorithm for solving max flow, which runs in time. We now compare the growth of vs for large and constant . The goal is to find out when becomes larger than .
We have:
Letting we need to solve If we take Wolfram Alpha indicates this inequality holds for all , which means . If the number of edges/vertices of the graph were to exceed this number, then Edmonds Karp would be faster. If the same calculations give us
It is also worth considering how much slower this algorithm is for a more reasonable value of . Note that because we do not know the value of the constant , it is unreasonable to analyze small values of (like or ). Taking if we assume our graph has edges, then we have:
Similar to the behavior of the function in Section 6.2, this function gets larger before it gets smaller. When it equals and of course these numbers would be larger for a larger Thus we can conclude that for any reasonably sized graph (where reasonably sized is defined as can fit in the storage of the world’s largest supercomputers), a traditional algorithm like Edmonds-Karp will be faster.
8 Conclusion
8.1 Summary
We began with preliminaries and provided a detailed list of all of the different subparts of the algorithm in [2] that would have to be implemented in order for the main algorithm to be implemented. After this, we discussed our implementations of portions of the algorithm, including the Low Stretch Decomposition, the Rebuilding Game and the Interior Point Method with insights into why certain implementation decisions were made. Finally, we examined the overall time complexity of the algorithm in detail and worked out a more precise asymptotic bound.
8.2 Future Directions of Study
Several portions of the algorithm in [2] remain unimplemented, including primarily the expander decomposition and a fully correct Interior Point Method. The nested list provided in Section 2.2 combined with the stubs provided in the GitHub should give a roadmap for any future attempts at implementing this algorithm. Separately, while the algorithm in [2] is the asymptotically fastest algorithm for max flow to date, as discussed in the final subsection it is not designed to be practical. Another interesting area of research would be to attempt to find the fastest algorithm which can be implemented reasonably or prove that existing algorithms already meet those criteria.
References
- [1] Ittai Abraham and Ofer Neiman. Using petal-decompositions to build a low stretch spanning tree. SIAM Journal of Computing, 48(2):227–248, 2019.
- [2] Li Chen, Rasmus Kyng, Yang Liu, Richard Peng, Maximilian Gutenberg, and Sushant Sachdeva. Maximum flow and minimum-cost flow in almost-linear time. 2022.
- [3] Julia Chuzhoy and Thatchaphol Saranurak. Deterministic algorithms for decremental shortest paths via layered core decomposition. Proceedings of the 2021 ACM-SIAM Symposium on Disrete Algorithms (SODA), pages 2478–2496, 2021.
- [4] Shimon Even and Yossi Shiloach. An on-line edge-deletion problem. Journal of the ACM, 28:1–4, 1981.
- [5] Thatchaphol Saranurak and Di Wang. Expander decomposition and pruning: Faster, stronger, and simpler. Proceedings of the Thirtieth Annual ACM-SIAM Symposium on Discrete Algorithms, SODA 2019, San Diego, California, USA, pages 2616–2635, 2019.
- [6] Daniel Sleator and Robert Tarjan. A data structure for dynamic trees. Journal of Computer and System Sciences, 26(3):362–391, 1983.
- [7] Daniel Sleator and Robert Tarjan. Self-adjusting binary search trees. Journal of the Association for Computing Machinery, 32(3):652–686, 1985.
- [8] Daniel Spielman and Shang-Hu Teng. Nearly-linear time algorithms for graph partitioning, graph sparsification, and solving linear systems. Proceedings of the 36th Annual ACM Symposium on Theory of Computing, STOC 2004, Chicago, IL, USA, pages 81–90, 2004.
- [9] Daniel Spielman and Shang-Hua Teng. Solving sparse, symmetric, diagonally-dominant linear system in time . 44th Annual IEEE Symposium on Foundations of Computer Science, pages 416–427, 2003.